决策:E2E 活栈在 runner 内用 docker compose 起全栈(postgres + redis + minio + backend), 与本地验证路径完全一致;未采纳「service containers + runner 上 mvn package 后 java -jar」—— 那需要为后端另写一套 host 网络配置,等于制造第二条与本地不一致的路径。 本机实测(与 CI 同一序列):中间件三项 healthcheck 全 Healthy(pg_isready / redis-cli ping / mc ready local)→ backend /api/v1/health/live 返回 200 → seed 幂等可执行(INSERT 0 0) → pytest tests/e2e → 49 passed。 (本机限制:Docker Hub 不可达且无 temurin 基础镜像,未复现 --build;改用本地既有镜像为底座、 挂载当日 16:24 构建的 jar,使被验证的代码版本与 CI 一致 —— 否则是拿 09-09 的旧代码验 09-18 的用例。) 修掉的 6 处缺陷: 1. minio/minio:latest 是滚动 tag → pin RELEASE.2025-09-07T16-13-09Z(否则 CI 拉到的镜像 与本地验证时不同,表现为不可复现的偶发失败) 2. up 未显式列出 minio(靠 depends_on 隐式拉起),而 minio 的 healthcheck 正是 backend 能否启动的前置条件 3. up --build 把构建与起栈耦合,失败日志混杂 → 拆为 build / up 两步 4. 就绪窗口 5min 偏紧(首次构建后仍需 Spring 启动 + Flyway 46 个迁移)→ 7.5min 5. 缺 down -v 前置清卷:seed 用 ON CONFLICT (username) DO NOTHING,残留卷会让账号口令 停留在上一轮 → 登录 401,并伪装成业务失败 6. job 无 timeout(GH Actions 默认 360 分钟)→ timeout-minutes: 30 另:pytest -v → pytest tests/e2e -v。原以为从仓库根收集会误收 tests/*_test.py (其在 import 期依赖 psycopg2 与 E2E_DB_PASSWORD),实测否定:pytest.ini 的 testpaths = tests/e2e 已限定范围,从根收集正是 49 个用例且无 error。显式传参作为兜底保留。 计划 Task 2.3 由「依赖基础设施决策」改为「已决策并落地」,仅余 Step 3(CI 上实测一次绿)。
16 KiB
Phase 23 收尾与 UAT 就绪实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 把「Phase 01~22 已关闭、Phase 23 部分关闭、后端 669 tests 全绿」这一停点推进到 Phase 24(UAT)可启动:清偿 Phase 23 剩余与代码级遗留,完成 UAT 前置(决策、用例、目视复验),并登记 Phase 25/26 前置项。
Architecture: 分五批推进 —— 批 13 不需要用户决策(可立即执行),批 4 需用户协作(目视复验 + 13 项决策),批 5 仅登记。批 13 每批完成后必须跑全部门禁;批 2 的「活栈供给方式」基础设施决策已于 2026-09-18 落地为「Compose in CI」(见 Task 2.3),本机同一序列实测 E2E 49 passed;仅余 Task 2.3 Step 3(GitHub Actions 上实测一次绿)需在有 Docker Hub 出口的环境完成。
Tech Stack: Java 21 / Spring Boot 3 / PostgreSQL / Flyway · Vue 3 + TypeScript + Vite · 微信小程序原生 TypeScript · Node.js(契约门禁)· Docker Compose · GitHub Actions
Spec: docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md(Phase 23~26);
基线: docs/superpowers/specs/2026-09-13-reference-driven-dual-client-ui-redesign-design.md(UI 唯一视觉基线)
当前停点(2026-09-18)
| 项 | 状态 |
|---|---|
| Phase 01~22 | ✅ 复验关闭 |
| Phase 23 | ⚠️ 部分关闭 —— 44 个 pass 空壳已清、4 个真实 INV 行为守卫已补;E2E 场景实现与 CI 串联未完成 |
| Phase 23B | ✅ E2E 测试基建重建(conftest + seed + CI e2e job,本机活栈 33→49 passed) |
| 横向加固 | ✅ HTTP 语义(105 处真实状态码)+ 身份收敛 + 幂等键(30 端点,V045/V046)+ 端口/入口守卫 |
| 后端回归 | 669 / 0 failures / 0 errors / 3 skipped |
| UI 契约门禁 | 14/14 绿 |
| 停点 | Phase 24(UAT)启动门前 —— 需用户确认 |
✅ 批 2 的 E2E 项已在本机真实执行(2026-09-18:
pytest tests/e2e→ 49 passed,真栈 + 真库 + 当日代码), 「失败为零可进入 UAT」的前置条件已满足。仍缺的是 E2E 在 CI 上跑过一次(Task 2.3 Step 3)—— 那是回归防线缺口,不阻塞 UAT 启动,但 Phase 23 因此仍记「部分关闭」。
Global Constraints
- 禁止用
rm/git rm删除文件 —— 本机有 safe-delete 钩子会把「删文件」放大为「删父目录」(2026-09-18 实测:git rm单个文件导致整个docs/被移除)。需要移除内容时一律原地重写(Write/Edit)。 - 不修改业务规则、不扩大 V1 范围;新增能力必须走 Phase 顺序。
- 每批完成后跑
node scripts/run-ui-contracts.mjs(14 个守卫,必须 14/14 绿;已接入 CI jobui-contracts)+ui-document-baseline-contracts.mjs(含端口与/admin/子路径断言)。 - 后端改动跑离线容器
mvn -o test,必须 0 failures / 0 errors。 - 提交纪律:每批独立 commit +
git tag(单段名,带/会被本机常驻进程清理)+ 回读验证;git bundle create覆盖最新提交。 - 涉及密码/密钥:不写入文档,只用环境变量或安全 Seed。
批 1 — 快速收尾(无需决策)✅ 已完成(Task 1.1 / 1.2 均于 2026-09-18 关闭)
Task 1.1: ui-tests 全量契约门禁接入 CI ✅(2026-09-18 已完成)
Files:
- Modify:
.github/workflows/ci.yml(新增 jobui-contracts) - Add:
scripts/run-ui-contracts.mjs(统一执行器:逐个跑 → 汇总 → 任一失败即非零退出) - Test:
ui-tests/*.mjs(实测 14 个守卫文件;原文此处误写「15 个」,本轮更正)
背景:14 个 UI 契约门禁此前只在本地手工执行,CI 上无人跑 —— 文档改了、端口漂移了、/admin/ 又退回 alias 了,CI 不会变红。这是「门禁存在但不生效」的同族问题(本项目已复发多次)。
- Step 1: 确认 CI 现有 job 清单(
backend/admin-web/miniapp/migration/test-shell-guard/e2e)与触发条件。 - Step 2: 新增
ui-contractsjob(node-version: 20,与既有 job 一致;跑node scripts/run-ui-contracts.mjs),任一守卫非 0 退出即 job 失败。 - Step 3: 负向验证:① 注入一个必然失败的守卫(放在
.workbuddy/tmp/、显式传参运行,不污染正式门禁)→ 执行器报 FAIL、退出码 1 并打印该守卫原始输出;② 守卫数量下限分支以UI_GUARDS_MIN=99自检 → 正确拒绝报绿。 - Step 4: 确认基线变绿:
PASS 14/14、退出码 0;ci.yml经yaml.safe_load解析通过、jobs 含ui-contracts。
说明:Step 3 用的是「合成失败守卫」而非「改坏 STATUS 端口」—— 后者验证的是单个守卫的判别力,而本 Task 要验证的是执行器与 CI 接线能否把失败传递出来(合成守卫足以隔离这一点)。端口/入口类守卫自身的判别力已在 2026-09-18「管理端入口收敛」轮以真实负向注入证明(6 条断言)。
Acceptance: CI 上 14 个契约全绿(本地已实测);注入缺陷后执行器非零退出、CI 变红。✅
Task 1.2: database 文档对齐 ✅(2026-09-18 已完成)
docs/database/TABLE_INVENTORY.md由 40 表重写为实测 59 表(含来源迁移列)。docs/database/MIGRATION_GUIDE.md由 V001~V011 重写为实测 V001~V046(46 个)。- 两处均标注「以
backend/src/main/resources/db/migration/为准」。
Acceptance: 文档表数与迁移文件数一致(59 / 46)。✅
批 2 — Phase 23 剩余(核心阻塞)
Task 2.1: E2E 场景真实实现
Files:
- Modify:
tests/e2e/**(现为占位文件,场景清单已完整保留在各文件头部) - Test: 本地活栈
pytest tests/e2e
场景清单(占位文件头部已列,不得缩减):
| 文件 | 场景数 | 覆盖 |
|---|---|---|
| 首页聚合 | 11 | 首页 8 入口顺序、Banner 仅内部跳转、今日推荐、公益服务、安全提醒 |
| 本地资格 | 11 | 声明 → 佐证上传 → 审核 → 状态流转;INV-011 未授权不可见 |
| 互选 | 8 | 二阶段同意:互选结果 → 双方分别确认 → 创建 Match;已 Match 复用 / 已 Unmatch 新建 |
| 权限 | 10 | 各角色数据范围(Counselor Assigned / ContentEditor 零用户数据 / 站点点范围) |
| 预约 | 7 | 用户创建/详情/列表/取消仅本人;代登记走独立 operator 接口 |
| 内容 | — | 婚恋课堂 / 反诈列表与详情 |
| 组织隔离 | — | 跨组织默认拒绝 |
- Step 1: 逐文件实现场景(先删除空
pass,禁止保留空壳)。 - Step 2: 本地活栈跑通(需后端 + PostgreSQL + 种子数据 +
E2E_PASSWORD)。 - Step 3: 负向验证:注入 1~2 处缺陷,确认对应 E2E 变红。
Acceptance: 场景全部真实执行且通过;无空 pass 残留。
Task 2.2: INV-011 显式守卫
Files:
- Create: 测试类(
backend/src/test/java/.../invariants/) - Read:
LocalEligibilityService、ReviewController
背景:LocalEligibilityService 全部方法以 userId 自限定、零 role 分支,佐证无独立服务端读取入口(仅经受权限控制的审核队列)—— 结构性满足,但无测试固化。
- Step 1: 写负向测试:非授权角色读取他人本地资格佐证 → 必须 403/不可达。
- Step 2: 确认真实落库路径(不是只断言常量,见
CoreInvariantsTest的教训)。 - Step 3: 负向注入验证守卫变红。
Acceptance: INV-011 有真实行为守卫。
Task 2.3: E2E 进入 CI(已决策并落地,2026-09-18)
Files:
- Modify:
.github/workflows/ci.yml(e2e job) - Modify:
infrastructure/docker-compose.yml(minio 镜像 pin)
决策(2026-09-18):在 runner 内用 Compose 起全栈。
备选是 service containers + 在 runner 上 mvn package 后 java -jar;但后者需为后端另写一套 host 网络配置,
等于制造第二条与本地不一致的路径,且增加一份与生产无关的分歧。Compose 全栈与本地验证路径完全相同,
且不需要 registry 凭据、不需要预构建镜像分发。
本机实测(同一序列,2026-09-18):中间件三项 healthcheck 全 Healthy(pg_isready /
redis-cli -a … ping / mc ready local)→ backend /api/v1/health/live 返回 200 → seed 幂等可执行
(INSERT 0 0)→ pytest tests/e2e → 49 passed。
本机限制(如实记录):Docker Hub 不可达(
auth.docker.ioBad Gateway)且本地无 temurin 基础镜像, 故未复现--build环节;改用本地既有镜像挂载当日 16:24 构建的 jar,使被验证的代码版本与 CI 一致 (否则是拿 09-09 的旧代码验 09-18 的用例,结论无效)。CI 上--build由 runner 完成。
- Step 1: 确定活栈供给方式 → Compose in CI(理由见上)。
- Step 2: 按决策实现 job:
compose config秒级快速失败 →down -v保证全新库 →build→up(显式含 minio)→ 就绪等待(90×5s)→ seed →pytest tests/e2e -v→ teardown;并加timeout-minutes: 30。 - Step 3: 实测一次完整的 CI 绿 —— 需在 GitHub Actions 上执行,本机无法进行(本机无 Docker Hub 出口)。
Acceptance: CI 串起 迁移 → backend test → admin type-check/build → miniapp type-check → E2E,任一失败阻断。
本轮顺带修掉的 6 个 CI 真实缺陷:
minio/minio:latest是滚动 tag(MinIO 已多次改动镜像内容,如移除内嵌 console)→ pinRELEASE.2025-09-07T16-13-09Z。否则 CI 拉到的镜像与本地验证时不同 → 不可复现的偶发失败。up未显式列出 minio(靠depends_on隐式拉起),而 minio 的 healthcheck 正是 backend 能否启动的前置条件 → 显式列出,消除隐性依赖。up --build把「构建镜像」与「起栈」耦合,失败时日志混杂 → 拆为build/up两步。- 就绪窗口 5 分钟偏紧(首次构建后仍需 Spring 启动 + Flyway 46 个迁移)→ 放宽到 7.5 分钟。
- 缺
down -v前置清卷:seed 用ON CONFLICT (username) DO NOTHING,若卷为上一轮残留,账号密码会停留在上一轮口令 → 本轮登录 401,并伪装成业务失败。(本机复用旧库时已实际踩到:必须手工UPDATE密码才能跑。)CI 每次全新库不触发,但 job 被取消/超时残留时会触发 → 起栈前强制清卷。 - e2e job 无 timeout(GH Actions 默认 360 分钟)→ 起栈失败会把流水线拖住数小时 →
timeout-minutes: 30。
未采纳的(避免过度设计):pytest -v 曾被认为会从仓库根误收集 tests/*_test.py(那些文件在 import 期即依赖 psycopg2 与 E2E_DB_PASSWORD)——实测否定:pytest.ini 的 testpaths = tests/e2e 已把范围限定,从根收集正是 49 个用例且无 error。仍显式写成 pytest tests/e2e -v,作为「该配置被改动」的兜底。
Task 2.4: Phase 22(Deployment / Ops)口径复验
⚠️ 口径澄清:此为计划的 Phase 22(部署运维基础设施),与
STATUS.md所述「Phase 22 部署运维配置扩展(Feature Flag)」是两份不同工作。后者已于 2026-09-18 复验关闭。
- 生产关闭 Mock WeChat / 默认密码 / Debug / Swagger / 演示 Seed;Secret 全部由环境变量或 Secret 文件注入。
- 备份加密 + 独立存放,执行一次 PostgreSQL + 对象存储的实际恢复演练(要有命令、日志、结果证据)。
- Rollback 明确回到哪个稳定镜像/版本;Flyway 只向前修复,不破坏 V001~V027。
- 删除 Backend 镜像「跳过测试」的发布路径;CI 先测试再打包。
Acceptance: staging 部署、健康检查、恢复演练、版本回滚均有可复现证据。
批 3 — 代码级遗留清偿
Task 3.1: Phase 21 遗留
- 隐私设置接口:
getPrivacySettings/updatePrivacySettings仍是 TODO 桩(返回假数据 / 不更新)→ 接真实user_privacy_settings。 - 注销红娘服务拦截硬编码
0:根因是数据模型没有matchmaker_services表(全仓搜索为空),拦截项在设计上无法落地。建表属扩权 → 需产品先确认红娘服务的存储模型。 - 数据保留流程:按
docs/legal/privacy-policy.md与docs/security/PIA.md落实保留期限(不得擅自永久保存或删除)。
Task 3.2: Phase 22 遗留(死配置)
counselingEnabled/premaritalEnabled/contentEnabled/serviceLocationEnabled/localEligibilityEnabled五个开关从未被业务读取 → 要么接线成真 gate,要么如实移除(不得留「配置暴露但未强制」的假开关)。counselingAttachmentsPrivate/Bucket私有桶死配置(存储实走object-storage.bucket-private)→ 同上。PUT /feature-flags不持久化 → 修复或明确标注为只读。
Task 3.3: 任务级 ⏳ 清偿
- P16-T13:互选超时接线(
expires_at已由 V041 提供)。 - P13A-T31:随访审计(
counseling_followups访问需审计)。 - P13A-T33:
RiskEvent source=counseling来源扩展。 - P04-T10:Onboarding 本地资格步骤。
批 4 — UAT 就绪(需用户协作)
Task 4.1: 管理端桌面端目视复验
- 用户按 1440px 基准检查管理端(
/admin/子路径部署)各页面;覆盖 Loading / Skeleton / Empty / Error / Permission / Disabled / Responsive 状态。 - 小程序按 375px 基准同上(首批 11 页已于 2026-09-16 验收,此处为增补页面)。
Task 4.2: PENDING_DECISIONS 13 项决策
- 产品确认
docs/PENDING_DECISIONS.md中 13 项待确认(人脸核验、本地资格保留期限、咨询资质、服务站开放时间、Banner 最大数量、推荐阈值等)。 - 复核新增的「第三方开放遗留」7 项(凭据机制 / 限流阈值 / OpenAPI 发布 /
POST /activities/{id}/interests语义 / 审计配额 / 脱敏边界 / 幂等 TTL 分级)。
Task 4.3: V1.1 分角色 UAT 用例编写
- 按 9 个角色(用户 / Reviewer / Matchmaker / Counselor / Counseling Manager / Service Station Operator / Content Editor / Activity Manager / Risk Operator)编写用例,替代 V1.0 旧用例(
tests/uat/UAT-Test-Cases.md为旧版参考)。 - 覆盖线上主路径、线下扫码、咨询、辅导、服务站、活动二阶段确认、反诈举报七条路径。
- 与
tests/uat/run-uat.sh(管理端可达性实测 + 备份检查)串联。
Task 4.4: Phase 24 启动确认
- 用户确认 Phase 24 启动;确认前不得自动进入。
批 5 — Phase 25 / 26 前置登记
- 运营素材清单(Phase 25):真实 Banner、服务站信息、咨询、课堂、反诈、关于我们、组织信息 —— 不 Seed 假用户 / 假 Match。
- 文案审核:隐私政策、用户协议、敏感文案、「非官方婚姻状态核验」提示。
- 上线前 13 项 smoke(Phase 26):Home / Quick Entries / Service Station / Counseling / Content / Activity Mutual / Chat / Block 等。
- RC 冻结:镜像、数据库版本、配置清单、第三方依赖、Rollback 目标。
验收与提交纪律
- 批 1~3 每批完成后:
ui-tests/*.mjs14/14 绿 +ui-document-baseline-contracts.mjs通过 + 后端mvn -o test0 failures。 - 批 2 的端到端前置已满足(2026-09-18 本机 E2E 49 passed);仍缺的是 E2E 进 CI 的首次实测绿(Task 2.3 Step 3)—— 不阻塞 UAT 启动,属回归防线缺口。
- 每批独立 commit;
git tag用单段名;git bundle create覆盖最新提交并回读验证。 docs/STATUS.md/docs/JIRA_BACKLOG.md/docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md随批同步。- 不做:不修改 Recovery 报告与
releases/冻结快照;不触碰 bundle 自保产物。
创建: 2026-09-18 — 承接 docs/文档治理报告-2026-09-18.md 与 STATUS.md 的两处悬空引用(D1)