Files
danyang-match/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md
T
WorkBuddy e666ee6f8a feat(e2e): Phase 23 Task 2.1 —— 活动互选 8 场景 + 服务预约 8 场景真实实现(16 个真实用例)
背景:`tests/e2e/activity_mutual_consent_test.py` 与 `service_appointment_test.py` 此前是
**诚实占位**(仅 docstring、0 用例,指针指向 run_v11_tests.py),计划 Task 2.1 要求把
文件头保留的场景清单逐条实现为真实 HTTP 用例。本提交完成该任务。

═══ 新增真实用例 16 个(本机活栈 pytest tests/e2e 49 → 65 passed)═══

activity_mutual_consent_test.py(8,覆盖 INV-014/015/006 + AGENTS.md #19/#20)
  1 建立互选记录不建 Match(INV-014)
  2 单方确认不建 Match(INV-015)
  3 双方确认建 Match Episode 且双方可见
  4 已有 active Match 时再次双确认必须复用(#19)
  5 曾 Unmatch 后重新双确认开启新 Episode:episode +1(#20)
  6 一方拒绝不建 Match,对方随后确认也不建
  7 超时:已过期记录不得被推动成双确认(含「未过期必须生效」对照,防假绿)
  8 单向意向不泄露(INV-006,含「B 确认后必须看到 accepted」反证)

service_appointment_test.py(8)
  1 提交预约;2 幂等键重放不得产生第二条;3 运营确认 submitted→confirmed(用户侧同态可见)
  4 并发「用户取消 × 运营确认」不得 5xx、最终状态必须收敛;5 我的列表只含本人
  6 待确认可取消;7 归属隔离(他人不可读详情/不可取消)
  8 运营侧状态闸门:已完成预约不得再被取消

═══ 两个幂等 seed(E2E 前置数据,本机与 CI 通用)═══

- seed_e2e_activity.sql:organization + 两个 activity。
  必须建的原因是硬约束而非选择:`activity_mutual_connection_consents.activity_id`
  有 **FK 到 activities(id)**(实测约束名 …_activity_id_fkey),而 activities.organizer_org_id
  又 FK 到 organizations(id) 且 NOT NULL —— 全新库里三张表全空,互选无活动可挂。
- seed_e2e_service_station.sql:service_location + **站点-运营关联**。
  关联必需:运营确认受数据范围约束(`assertOperatorScope` → `canAccessByOperator`),
  super_admin **不豁免**,实测 e2e_admin 确认他站预约返回 403。
  E2E 因此改用 seed 中该站点关联的 e2e_station。

两个 seed 由测试首用时幂等执行(docker exec psql),无需改 CI 步骤。

═══ 负向注入(Task 2.1 Step 3)═══

③ 撤销 service_station_operator 的 service_appointment.confirm →
   恰 2 红(test_operator_can_confirm / test_operator_cannot_cancel_completed,403),其余 6 绿;
④ 触发器强制新建互选记录的对方侧 accepted →
   3 红(both_confirm_creates_match_episode / timeout_handled / single_interest_not_leaked),其余 5 绿。
两处均已还原并复跑全绿(65 passed),确认无残留(无 e2e* 触发器、权限计数回到 1)。

**两次失败的注入(记录以免后人重复踩)**:
- 删除站点→运营关联:**无效** —— 测试的 seed fixture 在用例前幂等重放 seed 会自愈该前置条件;
- 改 user_b_status 列默认值:**无效** —— `createMutualConsent` 在 Java 侧显式写 pending,
  DB 默认值根本不参与(侧面证明写入路径的权威性)。

═══ 计划文档校正(审阅产物)═══

按实测推翻 4 处失实断言,登记 7 项新发现(详见计划文档「附 A / 附 B」):
- 推翻:matchmaker_services「无表/需扩权」(实为 V009 已建表且 mapper 齐备);
  门禁 14/14(实为 18/18);「tests/e2e 现为占位」(实为仅 2 个文件);
  E2E 基线 49(实为 65)。
- 新发现:① `POST /operator/organizations` 必然 500(createOrganization 不赋 id +
  IdType.INPUT + 无 DB 默认值;且未校验 chk_org_type)→ 连带「无法新建活动」;
  ② `createMutualConsent` 从不写 expires_at → 互选永不过期(即批 3 P16-T13,配置与
  V041 部分索引空转);③ 用户侧取消预约不校验状态(已确认/已完成也能自取消),
  与运营侧明确拒绝 completed 口径不一致 → 需产品裁定;
  ④ E2E 环境 organizations/activities/service_locations 均为空表(无迁移种子);
  ⑤ 运营数据范围 super_admin 不豁免(预期行为,勿误判);
  ⑥ 本机复跑会遇到 Redis 登录锁定键(auth:operator_login_lock:*);
  ⑦ e2e_* 账号因 ON CONFLICT DO NOTHING 重跑 seed 不覆盖旧口令。

═══ 验证 ═══

- `pytest tests/e2e` → **65 passed**(含新增 16);`node scripts/run-ui-contracts.mjs` → **18/18 PASS**,退出 0;
- 本机环境处置(不属代码改动):把 6 个 e2e_* 账号口令对齐到 CI 口令、清除 Redis 登录锁定键。
- **未验证**:微信开发者工具、CI 上的首次 E2E 实测绿(Task 2.3 Step 3,本机无 Docker Hub 出口)。
2026-09-19 12:28:16 +08:00

27 KiB
Raw Blame History

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 场景实现已完成(2026-09-19,见 Task 2.1),仅余 E2E 在 CI 上的首次实测绿
Phase 23B ✅ E2E 测试基建重建(conftest + seed + CI e2e job,本机活栈 49 → 65 passed)
横向加固 ✅ HTTP 语义(105 处真实状态码)+ 身份收敛 + 幂等键(30 端点,V045/V046)+ 端口/入口守卫
后端回归 669 / 0 failures / 0 errors / 3 skipped
UI 契约门禁 18/18 绿(原文 14/14 已过期,见 Global Constraints)
停点 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(守卫数量以执行器 MIN_GUARDS 为准;2026-09-19 实为 18 个,须 18/18 绿 —— 原文「14 个 / 14/14」已过期:2026-09-19 起相继新增 wxml-structure-contracts、miniapp-self-method-contracts、wxss-component-selector-contracts、miniapp-api-client-behavior)+ ui-document-baseline-contracts.mjs(含端口与 /admin/ 子路径断言)。
  • 后端改动跑离线容器 mvn -o test,必须 0 failures / 0 errors。
  • E2E 改动跑 pytest tests/e2e(需活栈;本机 E2E_BASE_URL=http://localhost:8085/api/v1、E2E_OPERATOR_PASSWORD 见 CI 变量),必须 0 failed。 本机复跑前若出现 429 账号已锁定,是 Redis 里的登录失败锁定键(auth:operator_login_lock:<operatorId>,TTL 15 分钟), 由前一轮失败登录累积触发 —— 删掉该键即可,不是业务缺陷。
  • 提交纪律:每批独立 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 不会变红。这是「门禁存在但不生效」的同族问题(本项目已复发多次)。

  • Step 1: 确认 CI 现有 job 清单(backend / admin-web / miniapp / migration / test-shell-guard / e2e)与触发条件。
  • Step 2: 新增 ui-contracts job(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 场景真实实现 ✅(2026-09-19 完成)

Files:

  • Modify: tests/e2e/activity_mutual_consent_test.py、tests/e2e/service_appointment_test.py
  • Add: tests/e2e/sql/seed_e2e_activity.sql、tests/e2e/sql/seed_e2e_service_station.sql
  • Test: 本地活栈 pytest tests/e2e

前置状态修正(2026-09-19 实测):原文「现为占位文件」只对上述两个文件成立 —— 它们在 2026-09-18 的 KEEP-VERIFY 轮已从「44 个 pass 空壳」改为诚实占位(仅 docstring、 0 个用例)。其余文件(home_v11_test.py / local_eligibility_test.py / counseling_* / http_semantics_test.py / idempotency_test.py 等)在 Phase 23B 已是真实用例,不是占位。

场景清单(占位文件头部已列,不得缩减):

文件 场景数 覆盖 实现状态
首页聚合 11 首页 8 入口顺序、Banner 仅内部跳转、今日推荐、公益服务、安全提醒 分散在 home_v11_test.py 等(场景编号与用例数非一一对应,未逐条对账)
本地资格 11 声明 → 佐证上传 → 审核 → 状态流转;INV-011 未授权不可见 分散在 local_eligibility_test.py 等(同上)
互选 8 二阶段同意:互选结果 → 双方分别确认 → 创建 Match;已 Match 复用 / 已 Unmatch 新建 ✅ activity_mutual_consent_test.py 8/8 通过
权限 10 各角色数据范围(Counselor Assigned / ContentEditor 零用户数据 / 站点点范围) 分散在 counseling_permission_test.py / service_station_permission_test.py / organization_isolation_test.py
预约 7 用户创建/详情/列表/取消仅本人;代登记走独立 operator 接口 ✅ service_appointment_test.py 8/8 通过(原「已确认不可自行取消」一条见下方说明,改为有规格支撑的归属隔离 + 运营侧状态闸门两条)
内容 — 婚恋课堂 / 反诈列表与详情 content_e2e_test.py
组织隔离 — 跨组织默认拒绝 organization_isolation_test.py
  • Step 1: 逐文件实现场景(先删除空 pass,禁止保留空壳)。两文件共 16 个真实用例,pass 空壳为 0。
  • Step 2: 本地活栈跑通(需后端 + PostgreSQL + 种子数据 + E2E_PASSWORD)。 本机实测 pytest tests/e2e → 65 passed(49 基线 + 16 新增)。新增两个幂等 seed 文件承载 「活动互选」与「服务站/站点-运营关联」的前置数据,本机与 CI 通用。
  • Step 3: 负向验证:注入 2 处缺陷,确认对应 E2E 变红(详见下方「负向注入记录」)。

Acceptance: 场景全部真实执行且通过;无空 pass 残留。✅

负向注入记录(Step 3,2026-09-19 实测)

# 注入 期望 实测
③ 撤销 service_station_operator 的 service_appointment.confirm 运营确认类用例变红 恰 2 个红(test_operator_can_confirm / test_operator_cannot_cancel_completed,HTTP 403),其余 6 绿
④ 触发器强制新建互选记录的对方侧为 accepted 依赖「对方未确认」的用例变红 3 个红(test_both_confirm_creates_match_episode / test_timeout_handled / test_single_interest_not_leaked),其余 5 绿

两处注入均已还原并复跑全绿(65 passed),且确认无残留(无 e2e* 触发器、权限计数回到 1)。

两次失败注入(记录在案,避免后人重复踩)

  • 注入①「删除站点→运营关联」无效:测试的 seed fixture 会在用例前幂等重放 seed, 把关联补回来 —— 即 fixture 会自愈它自己的前置条件。要注入必须选 fixture 不会修复的东西。
  • 注入②「把 user_b_status 列默认值改成 accepted」无效:createMutualConsent 在 Java 侧显式写入 pending,DB 默认值根本不参与。这条侧面证明了写入路径的权威性。

Task 2.2: INV-011 显式守卫

Files:

  • Create: 测试类(backend/src/test/java/.../invariants/)
  • Read: LocalEligibilityService、ReviewController

背景:LocalEligibilityService 全部方法以 userId 自限定、零 role 分支,佐证无独立服务端读取入口(仅经受权限控制的审核队列)—— 结构性满足,但无测试固化。

2026-09-19 实测落点(照此实现即可,不必重新勘察)

  • 当前唯一服务端读取入口:POST /api/v1/operator/reviews/{taskId}/evidences/{evidenceId}/access (ReviewController:165),两级 gate:类级 hasAuthority('review.view')

    • 方法级 hasAuthority('local_eligibility.evidence.view')。
  • 业务侧 ReviewService.accessLocalEligibilityEvidence(taskId, evidenceId, operatorId, purpose) 在 权限注解之外再校验四件事:purpose 非空 → 任务是 local_eligibility 类型 → requireClaimOwner(领取人本人)→ 佐证归属该任务用户的资格记录。 成功与拒绝都会写 sensitive_access_logs(logEvidenceAccess,result = success|denied)—— 这正是「可核查」的落库路径,也是负向断言应该咬住的地方。

  • 用户侧入口 /local-eligibility/{declaration,evidences,submit} 全部以 principal.id() 自限定,无 userId 入参。

  • 佐证上传前置:featureFlags.isLocalEvidenceUploadEnabled()、evidenceType 白名单、 fileId 必须属于该用户自己的 user_photos(userPhotoMapper.findByUserIdOrderBySort)。

  • 测试基建:@SpringBootTest @ActiveProfiles("test")(H2,schema-h2.sql),参照 V11InvariantsTest。

  • Step 1: 写负向测试:非授权角色读取他人本地资格佐证 → 必须 403/不可达。 (建议同时覆盖:非领取人、跨用户佐证、非 local_eligibility 任务、purpose 为空)

  • Step 2: 确认真实落库路径(不是只断言常量,见 CoreInvariantsTest 的教训)—— 即断言 sensitive_access_logs 真的新增了对应 result 的记录。

  • Step 3: 负向注入验证守卫变红。 ⚠️ 本机后端跑的是当日构建的 jar,改 Java 代码需重建镜像;注入应优先选 不需要重建的手段(如撤销 local_eligibility.evidence.view 的角色授予、 或直接改库内状态),否则该步骤成本极高。

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 完成。

  • 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 真实缺陷:

  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 表(全仓搜索为空),建表属扩权,需产品先确认」—— 三条均与实况不符,2026-09-19 实测推翻: ① 表存在,由 V009__add_matchmaker_tables.sql 创建(chk_matchmaker_status 白名单: submitted / assigned / in_service / waiting_user / completed / cancelled); ② MatchmakerServiceMapper、MatchmakerServiceRecord、MatchmakerServiceService 均已存在; ③ 因此不需要扩权、不需要产品裁定,属可直接实现的接线缺陷。 另:AccountLifecycleService.notifyMatchmaker(注销时通知红娘服务)同样是 TODO 空转。 位置:AccountLifecycleService.checkCanWithdraw 中 long activeMatchmakerServices = 0; // 需要 MatchmakerServiceMapper。 (原文「全仓搜索为空」很可能是只在 db/migration/ 内搜了表名的另一种写法 —— 该表在 demo-seed-*.sql 与 V009 中都有;凡「全仓无 X」这类断言必须给出搜索命令与范围, 否则无法复核,本项目已多次因此产生失实结论。)
  • 数据保留流程:按 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 18/18 绿(数量以执行器 MIN_GUARDS 为准)+ ui-document-baseline-contracts.mjs 通过 + 后端 mvn -o test 0 failures + pytest tests/e2e 0 failed。
  2. 批 2 的端到端前置已满足(2026-09-19 本机 pytest tests/e2e → 65 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-19 审阅校正与新发现

A. 校正(本文件中已被实测推翻的断言)

位置 原文 实测
Task 3.1 第 2 项 matchmaker_services 表不存在、建表属扩权、需产品确认 表由 V009 创建;mapper/entity/service 齐备;无需扩权,可直接接线
Global Constraints / 验收纪律 / 当前停点 UI 契约门禁 14 个、14/14 绿 18 个(2026-09-19 相继新增 4 项守卫),基线 18/18
Task 2.1 tests/e2e/** 现为占位文件 仅 2 个文件是占位;其余在 Phase 23B 已是真实用例
当前停点 本机活栈 E2E 49 passed 基线 49,Task 2.1 完成后 65 passed

B. 新发现(不在本计划内,需另行处置)

  1. POST /operator/organizations 必然 500(High) OrganizationService.createOrganization 从不给 id 赋值,而 BaseEntity 用 @TableId(type = IdType.INPUT)、organizations.id 无 DB 默认值 → invalid input syntax for type uuid: ""。且该接口未校验 type, 会撞生产 chk_org_type(白名单:women_federation / trade_union / charity_center / volunteer_team / civil_affairs_partner / other),大概率在 PG 上再炸一次。 影响:运营端无法新建机构 → 无机构则无法新建活动(activities.organizer_org_id NOT NULL)。
  2. 互选记录永不过期(Medium,即批 3 Task 3.3 的 P16-T13) createMutualConsent 从不写 expires_at(列无默认值),而 confirmSideIfPending 的过期闸门 expires_at IS NULL OR expires_at > NOW() 对 NULL 恒为真 → 配置项 default-mutual-deadline-hours: 48 与 V041 的部分索引 idx_mutual_consents_expiry 均为空转。E2E 的超时用例因此只能靠回拨库内值触发。
  3. 用户侧取消预约不校验状态(Medium,需产品裁定) ServiceAppointmentService.cancelAppointment 只校验归属,已确认/已完成的预约 都能被本人置为 cancelled;而运营侧 cancelAppointmentByOperator 明确拒绝 completed / cancelled。两侧口径不一致。原空壳用例名 test_cancel_after_confirmation_denied 无任何规格支撑(需求基线只写了归属隔离), 故未被写成断言 —— 待产品确认后再定。
  4. E2E 环境缺前置数据(Low,已由本轮 seed 解决) 全新库 organizations / activities / service_locations 均为空表—— 无任何迁移种子。已补 tests/e2e/sql/seed_e2e_activity.sql、 tests/e2e/sql/seed_e2e_service_station.sql(幂等,本机与 CI 通用)。
  5. 运营确认受数据范围约束,super_admin 不豁免(设计事实,易被误判为缺陷) assertOperatorScope → canAccessByOperator: sa.assigned_operator_id = :op OR EXISTS(service_location_operators 关联该站点 × :op)。 实测 e2e_admin(super_admin)确认他站预约返回 403 —— 这是预期行为, 不是权限配置错误。E2E 因此必须用站点关联的运营账号。
  6. 本机 E2E 复跑会被登录锁定拦住(Low,环境事项) Redis 键 auth:operator_login_lock:<operatorId>(TTL 15 分钟)由失败登录累积触发, 表现为 429 账号已锁定,请 N 分钟后重试。清键即可。CI 每轮全新 Redis 不受影响。
  7. e2e_* 账号口令与 CI 不一致(Low,环境事项) seed_e2e_operators.sql 用 ON CONFLICT (username) DO NOTHING,重跑 seed 不覆盖旧口令; 本机活栈若复用旧卷,口令会停在上一轮的值 → 大量 401。 本机已用 UPDATE operators SET password_hash = crypt('<CI 口令>', gen_salt('bf',8)) WHERE username LIKE 'e2e\_%' 对齐到 CI 口令。(这与 Task 2.3 顺带修的 CI 缺陷 #5 是同一根因的两面。)

创建: 2026-09-18 — 承接 docs/文档治理报告-2026-09-18.md 与 STATUS.md 的两处悬空引用(D1) 审阅校正: 2026-09-19 — 按实测推翻 4 处失实断言(见附 A),登记 7 项新发现(见附 B)