Files
danyang-match/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md
T
WorkBuddy f63e579a8d ci(e2e): 落地「活栈供给方式」决策 — Compose in CI,并修掉 6 处真实缺陷
决策: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 上实测一次绿)。
2026-09-18 21:58:56 +08:00

227 lines
16 KiB
Markdown
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.
# 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:** 分五批推进 —— 批 1~3 **不需要用户决策**(可立即执行),批 4 需**用户协作**(目视复验 + 13 项决策),批 5 仅登记。批 1~3 每批完成后必须跑全部门禁;**批 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 job `ui-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`(新增 job `ui-contracts`)
- Add: `scripts/run-ui-contracts.mjs`(统一执行器:逐个跑 → 汇总 → 任一失败即非零退出)
- Test: `ui-tests/*.mjs`(**实测 14 个**守卫文件;原文此处误写「15 个」,本轮更正)
**背景**:14 个 UI 契约门禁此前**只在本地手工执行**,CI 上无人跑 —— 文档改了、端口漂移了、`/admin/` 又退回 alias 了,CI 不会变红。这是「门禁存在但不生效」的同族问题(本项目已复发多次)。
- [x] **Step 1:** 确认 CI 现有 job 清单(`backend` / `admin-web` / `miniapp` / `migration` / `test-shell-guard` / `e2e`)与触发条件。
- [x] **Step 2:** 新增 `ui-contracts` job(`node-version: 20`,与既有 job 一致;跑 `node scripts/run-ui-contracts.mjs`),**任一守卫非 0 退出即 job 失败**。
- [x] **Step 3:** 负向验证:① 注入一个**必然失败的守卫**(放在 `.workbuddy/tmp/`、显式传参运行,不污染正式门禁)→ 执行器报 **FAIL、退出码 1** 并打印该守卫原始输出;② **守卫数量下限**分支以 `UI_GUARDS_MIN=99` 自检 → 正确拒绝报绿。
- [x] **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 已完成)
- [x] `docs/database/TABLE_INVENTORY.md` 由 **40 表**重写为实测 **59 表**(含来源迁移列)。
- [x] `docs/database/MIGRATION_GUIDE.md` 由 **V001~V011** 重写为实测 **V001~V046(46 个)**。
- [x] 两处均标注「**以 `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.io` Bad Gateway)且本地无 temurin 基础镜像,
> 故**未复现 `--build` 环节**;改用本地既有镜像挂载当日 16:24 构建的 jar,使**被验证的代码版本与 CI 一致**
> (否则是拿 09-09 的旧代码验 09-18 的用例,结论无效)。CI 上 `--build` 由 runner 完成。
- [x] **Step 1:** 确定活栈供给方式 → **Compose in CI**(理由见上)。
- [x] **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 真实缺陷**:
1. `minio/minio:latest` 是**滚动 tag**(MinIO 已多次改动镜像内容,如移除内嵌 console)→ **pin** `RELEASE.2025-09-07T16-13-09Z`。否则 CI 拉到的镜像与本地验证时不同 → 不可复现的偶发失败。
2. `up` **未显式列出 minio**(靠 `depends_on` 隐式拉起),而 minio 的 healthcheck 正是 backend 能否启动的前置条件 → 显式列出,消除隐性依赖。
3. `up --build` 把「构建镜像」与「起栈」耦合,失败时日志混杂 → 拆为 `build` / `up` 两步。
4. 就绪窗口 5 分钟**偏紧**(首次构建后仍需 Spring 启动 + Flyway 46 个迁移)→ 放宽到 7.5 分钟。
5. **缺 `down -v` 前置清卷**:seed 用 `ON CONFLICT (username) DO NOTHING`,若卷为上一轮残留,账号密码会停留在上一轮口令 → 本轮登录 401,并**伪装成业务失败**。(本机复用旧库时已实际踩到:必须手工 `UPDATE` 密码才能跑。)CI 每次全新库不触发,但 job 被取消/超时残留时会触发 → 起栈前强制清卷。
6. 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. 批 1~3 每批完成后:`ui-tests/*.mjs` **14/14 绿** + `ui-document-baseline-contracts.mjs` 通过 + 后端 `mvn -o test` 0 failures。
2. 批 2 的**端到端前置已满足**(2026-09-18 本机 E2E 49 passed);仍缺的是 **E2E 进 CI 的首次实测绿**(Task 2.3 Step 3)—— 不阻塞 UAT 启动,属回归防线缺口。
3. 每批独立 commit;`git tag` 用**单段名**;`git bundle create` 覆盖最新提交并回读验证。
4. `docs/STATUS.md` / `docs/JIRA_BACKLOG.md` / `docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md` 随批同步。
5. **不做**:不修改 Recovery 报告与 `releases/` 冻结快照;不触碰 bundle 自保产物。
---
*创建: 2026-09-18 — 承接 `docs/文档治理报告-2026-09-18.md` 与 STATUS.md 的两处悬空引用(D1)*