Files
danyang-match/ui-tests/ui-contracts.mjs
T
WorkBuddy d3735e3afa feat(matchmaker,contactauth): 受控联系授权机制立项落地(OQ-MATCHMAKER-003 / DEC-MATCHMAKER-003 B1 例外通道)
后端:
- 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
2026-10-11 13:08:02 +08:00

92 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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')