后端: - V056 matchmaker_contact_authorizations 表:channel CHECK(phone/offline_meeting/full_contact)、六态状态机(pending_approval→pending_consent→active→expired/revoked, →rejected)、部分唯一索引 uq_mm_contact_auth_active 防同红娘-同用户-同渠道多 ACTIVE、双态惰性收敛(active 过期 + pending_consent 超同意窗口) - ContactAuthorizationConfig 五 canonical key(matchmaker.contact.*):enabled 代码缺省 false fail-safe(yml 显式 true)/approver-authorities 双岗并列/consent-deadline-hours 72/default-validity-days 7/retention-days 365,全部 DEFAULT+CONFIG 待产品+法务终审 - ContactAuthorizationPolicy.assertContactAllowed(404 化 deny 先审计后 404) + canContact 徽标查询 - ContactAuthorizationService:request(须有效分配+purpose 必填)/approve(权限码动态判定∈配置+组长组织范围)/consent(用户本人,仅 pending_consent)/decline/revoke(reason 必填)/disclose(唯一披露出口:enabled fail-safe→assertActiveAssignment→assertContactAllowed→仅返回 phone/disclosedAt/authorizationId 三键,每次落 SensitiveAccessLog user_phone_disclosure,phone 不落应用日志) - 双控制器:运营端 /operator/matchmaker/contact-authorizations + C 端 /contact-authorizations(UserPrincipal 归属校验,独立端点无预同意参数),不碰 SecurityConfig 守卫翻转(机械锁强制复审机制按设计生效): - MatchmakerContactGuardTest 由「锁不存在」翻转为「锁新契约」五条:回显白名单类收敛/端点前缀收敛(受治理三包)/到期语义/consent 独立性三端扫描(后端+miniapp+admin-web,修复原只扫后端的预同意参数逃逸缺口)/ReferralService 白名单与无 @Scheduled 保留 - run-backend-contracts CURATED 18→20 前端: - miniapp 新页 pages/contact-consent(目的/渠道/窗口明示+独立同意拒绝按钮无预勾选,app.json 第 64 页) + 红娘工作台申请/列表/撤销 - admin-web「联系授权审批」tab(审批岗队列+本人授权撤销,枚举码中文映射) 验证:mvn verify 895/895(3 skip)、UI 契约 24/24(64 页)、后端契约门禁 228/CURATED 20、小程序真编译 87 WXML 0 失败;负向注入 5(工程师)+2(QA 不同落点)判红→还原复绿;QA 抓 disclose 键数收敛断言缺口已补并回归;QA fresh eyes PASS 0 P0/P1
92 lines
4.7 KiB
JavaScript
92 lines
4.7 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { existsSync, readFileSync } from 'node:fs'
|
||
import { resolve } from 'node:path'
|
||
|
||
const root = resolve(import.meta.dirname, '..')
|
||
const read = (path) => readFileSync(resolve(root, path), 'utf8')
|
||
|
||
function assertInOrder(source, labels, scope) {
|
||
let previous = -1
|
||
for (const label of labels) {
|
||
const position = source.indexOf(label)
|
||
assert.notEqual(position, -1, `${scope}缺少“${label}”`)
|
||
assert.ok(position > previous, `${scope}中“${label}”顺序错误`)
|
||
previous = position
|
||
}
|
||
}
|
||
|
||
const appConfig = JSON.parse(read('miniapp/app.json'))
|
||
// ZC-MM-003(B1 立项):contact-consent 联系授权确认页(用户单独明示同意)→ 63 → 64
|
||
assert.equal(appConfig.pages.length, 64, '小程序页面清单必须完整覆盖 64 页')
|
||
const redesignMatrix = read('docs/UI_REDESIGN_PAGE_MATRIX_V1.1.md')
|
||
for (const page of [
|
||
'pages/index/index', 'pages/discover/discover', 'pages/messages/messages',
|
||
'pages/activity/activity', 'pages/mine/mine', 'pages/matchmaker/matchmaker',
|
||
'pages/counseling-intro/counseling-intro', 'pages/premarital-intro/premarital-intro',
|
||
'pages/content/content', 'pages/anti-fraud/anti-fraud', 'pages/service-station/service-station',
|
||
]) {
|
||
assert.ok(redesignMatrix.includes(`\`/${page}\``), `首批页面矩阵遗漏 /${page}`)
|
||
}
|
||
assert.deepEqual(
|
||
appConfig.tabBar.list.map((item) => item.text),
|
||
['首页', '找对象', '消息', '活动', '我的'],
|
||
'底部导航顺序必须固定',
|
||
)
|
||
|
||
const requiredPages = [
|
||
'pages/service-center/service-center',
|
||
'pages/service-station/service-station',
|
||
'pages/service-appointment/service-appointment',
|
||
'pages/my-appointments/my-appointments',
|
||
'pages/content/content',
|
||
'pages/content-detail/content-detail',
|
||
'pages/anti-fraud/anti-fraud',
|
||
'pages/service-program/service-program',
|
||
'pages/service-history/service-history',
|
||
]
|
||
for (const page of requiredPages) {
|
||
assert.ok(appConfig.pages.includes(page), `小程序未注册页面 ${page}`)
|
||
for (const extension of ['json', 'wxml', 'wxss']) {
|
||
assert.ok(existsSync(resolve(root, `miniapp/${page}.${extension}`)), `小程序页面文件缺失:${page}.${extension}`)
|
||
}
|
||
assert.ok(
|
||
existsSync(resolve(root, `miniapp/${page}.ts`)) || existsSync(resolve(root, `miniapp/${page}.js`)),
|
||
`小程序页面缺少 TypeScript 或 JavaScript 逻辑源:${page}`,
|
||
)
|
||
}
|
||
|
||
const sharedServiceStyle = read('miniapp/styles/service-page.wxss')
|
||
assert.doesNotMatch(sharedServiceStyle, /@import\s+["']\.\.\/\.\.\/app\.wxss["']/, '共享页面样式不能错误导入 ../../app.wxss')
|
||
const homeMarkup = read('miniapp/pages/index/index.wxml')
|
||
assert.doesNotMatch(homeMarkup, /<[^>]+wx:elif="\{\{recommendations\.length\}\}"[^>]+wx:for=/, '推荐条件分支与循环必须分离,避免 wx:else 失联')
|
||
assert.match(homeMarkup, /wx:elif="\{\{recommendations\.length\}\}"/, '推荐列表必须有独立条件分支')
|
||
|
||
const homeLogic = read('miniapp/pages/index/index.ts')
|
||
assertInOrder(homeLogic, [
|
||
"label: '找对象'", "label: '公益红娘'", "label: '公益活动'", "label: '婚恋咨询'",
|
||
"label: '婚恋课堂'", "label: '婚前辅导'", "label: '婚恋反诈'", "label: '善缘服务站'",
|
||
], '首页核心入口')
|
||
assert.doesNotMatch(homeLogic, /功能开发中/, '首页核心入口不得使用“功能开发中”占位')
|
||
|
||
const activityDetailMarkup = read('miniapp/pages/activity-detail/activity-detail.wxml')
|
||
assert.doesNotMatch(activityDetailMarkup, /页面:\s*activity-detail/, '活动详情页不得是占位页面')
|
||
assert.match(activityDetailMarkup, /活动信息/, '活动详情页必须展示活动信息')
|
||
assert.match(activityDetailMarkup, /报名须知/, '活动详情页必须展示报名须知')
|
||
|
||
const adminLayout = read('admin-web/src/layouts/MainLayout.vue')
|
||
assert.match(adminLayout, /善缘丹阳/, '后台品牌名称必须为善缘丹阳')
|
||
|
||
const adminApiContracts = [
|
||
['admin-web/src/modules/banner/BannerManagementView.vue', '/operator/banners'],
|
||
['admin-web/src/modules/service-station/ServiceStationView.vue', '/operator/service-locations'],
|
||
['admin-web/src/modules/service-appointment/ServiceAppointmentView.vue', '/service-appointments/operator/list'],
|
||
['admin-web/src/modules/activity/ActivityInterestView.vue', '/activities/operator/activity-interest-summary'],
|
||
]
|
||
for (const [file, endpoint] of adminApiContracts) {
|
||
assert.ok(read(file).includes(endpoint), `${file} 未使用实际后台接口 ${endpoint}`)
|
||
}
|
||
assert.doesNotMatch(read('admin-web/src/modules/risk/ReportCenterView.vue'), /tableData\.value = res \|\|/, '举报列表不能把响应信封当数组')
|
||
assert.doesNotMatch(read('admin-web/src/modules/appeal/AppealCenterView.vue'), /tableData\.value = res \|\|/, '申诉列表不能把响应信封当数组')
|
||
|
||
console.log('UI contracts passed')
|