## 背景
2026-09-13 UI 重设计方案的 13 个契约门禁中 3 个为红,逐一查明性质后分类处理。
## 1. activity 页迁移 TypeScript(11/11 页现已全 TS)
- activity.js → activity.ts(139 行),业务逻辑零改动,仅补类型
- 此前 STATUS.md 记「9/11 TS-only、3 页待迁」,但 messages/matchmaker
已在 4f91be0 迁完,实际只剩 activity 一页,本提交后清零
## 2. miniapp-reference-contracts 契约自身过期(非代码问题)
- 契约引用 messages.js / matchmaker.js / activity.js,但页面已 TS 化,
readFileSync 抛裸 ENOENT 栈,看不出「契约引用了陈旧文件名」这一真实原因
- 改为统一走 readPageLogic(page):断言 .ts 存在 + .js 不存在(锁单一运行源),
找不到文件时给出可读断言而非崩溃
- 核心 11 页循环内统一加 TS-only 校验,防止 JS 回流
## 3. phase07-home-contracts 契约断言架构写反(非代码问题)
- 契约要求 HomeApplicationService 字面出现 userIdentityMapper.findByUserId,
但实际是 capabilityService.isIdentityVerified(userId)(HomeApplicationService:116)
- 这符合 AGENTS.md #26「前端入口可见性由服务端 CapabilityService 返回决定」,
让首页穿透 Capability 直接持有 mapper 反而是架构重复
- 且契约自身另一条已断言 CapabilityService 必须有 isIdentityVerified,
两条要求互相矛盾
- 改为:查库断言落在 caps 侧;首页断言必须委托 capabilityService.isIdentityVerified;
新增 doesNotMatch(home, /userIdentityMapper/) 禁止首页绕过 Capability
- 保留原有 doesNotMatch(home, /setVerified\(true\)/) 防硬编码断言
## 4. 新增 11 页静态预览器(本机无微信开发者工具)
- scripts/preview-miniapp.mjs:把 WXML + WXSS 转成可浏览器打开的静态 HTML
- 能力:wx:if/wx:elif/wx:else 链、wx:for 列表、嵌套三元、比较/取反运算、
自定义组件按属性展开、slot 替换、rpx→px 换算、组件样式收集
- mock 数据严格对齐 types/index.d.ts 与各页 WXML 实际读取字段
(如 regionName/heightCm/educationCode、status: registration_open),
不用想象字段,避免误导验收判断
- 产出 artifacts/ui-redesign/miniapp/preview.html(11 页可切换,284K)
- 明确边界:非真机渲染、不执行页面 JS、不连后端,
只用于视觉走查,不得作为功能验收证据
## 验证
- 契约门禁 12/13 通过(唯一红的 ui-document-baseline-contracts 涉及
STATUS.md 验收口径,待用户看真机效果后裁决,本轮不动)
- 11 页渲染体量:116/70/47/56/94/68/39/39/41/37/38 个类名元素,
零残留 {{}} 插值、零残留原始组件标签
- 小程序 tsc --noEmit 通过
61 lines
4.1 KiB
JavaScript
61 lines
4.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import fs from 'node:fs'
|
||
|
||
const read = (path) => fs.readFileSync(path, 'utf8')
|
||
const home = read('backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java')
|
||
const caps = fs.existsSync('backend/src/main/java/com/danyang/match/platform/capability/service/CapabilityService.java')
|
||
? read('backend/src/main/java/com/danyang/match/platform/capability/service/CapabilityService.java')
|
||
: home // fallback: CapabilityService 独立化前逻辑在 HomeApplicationService 里
|
||
const miniapp = read('miniapp/pages/index/index.ts')
|
||
const wxml = read('miniapp/pages/index/index.wxml')
|
||
const project = JSON.parse(read('project.config.json'))
|
||
|
||
const labels = ['找对象', '公益红娘', '公益活动', '婚恋咨询', '婚恋课堂', '婚前辅导', '婚恋反诈', '善缘服务站']
|
||
let previous = -1
|
||
for (const label of labels) {
|
||
const position = home.indexOf(`"${label}"`)
|
||
assert.ok(position > previous, `首页入口顺序错误: ${label}`)
|
||
previous = position
|
||
}
|
||
|
||
// 首页实名状态必须来自数据库,但按 AGENTS.md #26 只能经由 CapabilityService 委托,
|
||
// 首页不得自行持有 userIdentityMapper 复制业务规则。查库断言落在 caps(CapabilityService)侧。
|
||
assert.match(caps, /userIdentityMapper\.findByUserId/, 'Capability 实名状态必须来自数据库')
|
||
assert.match(home, /capabilityService\.isIdentityVerified/, '首页实名状态必须委托 CapabilityService(AGENTS.md #26)')
|
||
assert.doesNotMatch(home, /userIdentityMapper/, '首页不得绕过 CapabilityService 直接查实名表')
|
||
assert.match(home, /recommendationEngineService\.getDailyRecommendations/, '首页推荐必须来自正式推荐引擎')
|
||
assert.match(home, /contentArticleMapper\.findPublishedAll/, '首页课堂内容必须来自内容模块')
|
||
assert.match(home, /serviceLocationMapper\.selectOne/, '首页服务站必须来自服务站模块')
|
||
assert.match(caps, /setDisabledReason/, '不可用入口必须返回服务端原因')
|
||
assert.match(caps, /isIdentityVerified/, 'Capability 必须校验真实实名状态')
|
||
assert.match(caps, /ACCOUNT_STATUS_NORMAL/, 'Capability 必须拒绝非正常账户')
|
||
assert.match(home, /\/pages\/counseling-intro\/counseling-intro/, '咨询服务建议必须跳转真实页面')
|
||
assert.match(home, /\/pages\/premarital-intro\/premarital-intro/, '婚前辅导建议必须跳转真实页面')
|
||
assert.doesNotMatch(home, /"pages\//, '服务端返回的小程序路由必须以斜杠开头')
|
||
assert.doesNotMatch(home, /setVerified\(true\)/, '禁止硬编码实名通过')
|
||
assert.doesNotMatch(home, /card\.setId\("default"\)/, '禁止硬编码服务站')
|
||
|
||
assert.deepEqual(project.setting.useCompilerPlugins, ['typescript'])
|
||
assert.ok(fs.existsSync('miniapp/pages/index/index.ts'))
|
||
assert.ok(!fs.existsSync('miniapp/pages/index/index.js'), '首页只能保留 TypeScript 运行源')
|
||
assert.ok(fs.existsSync('miniapp/pages/discover/discover.ts'))
|
||
assert.ok(!fs.existsSync('miniapp/pages/discover/discover.js'), '找对象页只能保留 TypeScript 运行源')
|
||
assert.match(miniapp, /disabledReason/, '小程序必须显示服务端 Disabled 原因')
|
||
assert.match(miniapp, /loadError: true/, '首页必须有网络错误状态')
|
||
assert.match(miniapp, /matchStore\.like\(userId\)/, '首页喜欢操作必须调用正式关系服务')
|
||
assert.match(miniapp, /matchStore\.pass\(userId\)/, '首页略过操作必须调用正式关系服务')
|
||
assert.doesNotMatch(miniapp, /setTimeout\(/, '首页不得保留演示用延时反馈')
|
||
assert.match(miniapp, /QUICK_ENTRIES\.map/, '服务端入口必须按固定业务顺序合并')
|
||
assert.match(wxml, /home-hero-v2\.jpg/, '首页默认 Banner 必须使用参考图夜景主视觉')
|
||
assert.doesNotMatch(wxml, />›</, '首页不得继续使用字符箭头')
|
||
|
||
const sections = ['brand-bar', 'status-strip', 'banner-section', 'entries-card', '今日推荐', '善缘公益服务', '近期公益活动', '善缘服务站', '婚恋课堂', '安全提醒']
|
||
previous = -1
|
||
for (const section of sections) {
|
||
const position = wxml.indexOf(section)
|
||
assert.ok(position > previous, `首页区块顺序错误: ${section}`)
|
||
previous = position
|
||
}
|
||
|
||
console.log('Phase 07 home contracts passed')
|