# 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 场景实现已完成(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:`,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 不会变红。这是「门禁存在但不生效」的同族问题(本项目已复发多次)。 - [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 场景真实实现 ✅(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` | - [x] **Step 1:** 逐文件实现场景(**先删除空 `pass`**,禁止保留空壳)。两文件共 16 个真实用例,`pass` 空壳为 0。 - [x] **Step 2:** 本地活栈跑通(需后端 + PostgreSQL + 种子数据 + `E2E_PASSWORD`)。 本机实测 `pytest tests/e2e` → **65 passed**(49 基线 + 16 新增)。新增两个幂等 seed 文件承载 「活动互选」与「服务站/站点-运营关联」的前置数据,本机与 CI 通用。 - [x] **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 完成。 - [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` 表**(全仓搜索为空),建表属扩权,需产品先确认」—— **三条均与实况不符,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:`(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('', 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)*