Files
danyang-match/docs/Phase-13-V11-Recovery-Completion-Report-2026-09-16.md
T
WorkBuddy 5b3bad326e fix(matchmaker,referral): close Phase 13 KEEP-VERIFY/EXTEND — 7 real defects
KEEP-VERIFY/EXTEND 复验(历史 ✅ 不可信;测试覆盖函数 ≠ 覆盖运行时路径)。
逐条核对运行时路径与生产约束后修复 7 处缺陷,其中 3 处「只在真实 PostgreSQL 上炸」。

D1 (Critical, 生产故障)
  matchmaker_recommendations.status 代码写 liked/passed/confirmed_mutual,
  而 V009 chk_rec_status 只允许 pending_view/viewed/responded 且从未扩展
  → 真实 PG 上用户接受/跳过推荐必崩。测试库未复刻该 CHECK,故长期静默。

D2 (Critical, 生产故障)
  控制器缺省 serviceType="recommendation" 不在 chk_matchmaker_service_type
  白名单 → 提交红娘服务必崩。
  D1+D2 同修于新增 V038__align_matchmaker_status_constraints.sql
  (沿用 V018/V020/V025 的 DROP CONSTRAINT IF EXISTS + 重加白名单惯例)。

D3 (High, 越权写)
  MatchmakerServiceService.recommendPartner 无 assignedOperatorId 归属校验,
  且可把用户推荐给自己 → 补校验 + 禁止自我推荐。

D4 (High, 越权读)
  getServiceDetail 无归属校验且返回完整用户资料(userId/description/
  userProfile/followups)→ 签名加 requesterId,按调用者返回完整/脱敏视图。

D5 (High, 越权写)
  ReferralService 的 userId 取自请求体且零授权校验 → 新增
  assertMatchmakerServesUser,在 referToServiceStation/referToActivity 入口
  强制校验存在进行中的服务关系。

D6 (High, 生产故障)
  MatchmakerFollowupMapper.findByServiceId 只有声明无 @Select
  → Invalid bound statement (not found),getServiceDetail 必崩。
  该缺陷由本轮新增测试首次暴露(历史测试从未调用该路径)→ 补 @Select。

D7 (Medium, 契约缺口)
  admin-web 5 处 API 缺口(4 条 404 + C 端端点被运营端误用)+ 小程序缺转介服务层
  → 后端补 GET /operator/matchmaker/recommendations 与
  GET /referrals/operator/history 两个运营端端点;前端改路径、
  assign 改 POST + body operatorId;新增 miniapp/services/referralService.js。

测试保真度
  - 测试库补 matchmaker_services/recommendations/followups 三表(此前该模块零真实覆盖),
    约束与 V038 对齐;EntityMigrationParityTest 三表移入 REQUIRED_IN_TEST_SCHEMA;
    MigrationValidationTest LATEST_MIGRATION 37 → 38。
  - 新增 MatchmakerReferralPhase13Test(17 用例,@SpringBootTest 走真实方法路径 + 真实落库)。
  - 重写 MatchmakerServiceTest(6 用例):直接解析 db/migration/V*.sql,按文件名顺序应用
    DROP CONSTRAINT IF EXISTS + ADD CONSTRAINT 取"最终有效"白名单,并内置解析器自检。
    【关键】首版守卫硬编码白名单副本,注入 D1 后仍全绿(对漂移零反应)→ 必须解析真实 DDL。
  - 删除 3 处假测试占位(MatchmakerServiceTest.testMatchmakerCannotLike 的 assertTrue(true);
    CoreInvariantsTest INV-007 断言的是状态常量、INV-010 亦为 assertTrue(true)),
    已如实改名并注明行为验证位置。

负向验证(两轮)
  注入 D1(回退 chk_rec_status 白名单)→ 行为守卫 DataIntegrityViolation
  + 常量守卫 Multiple Failures (3 failures) → Tests run: 23, Failures: 1, Errors: 2
  / BUILD FAILURE,守卫有效性得证。缺陷已还原。

全量回归(离线容器,实测)
  [WARNING] Tests run: 391, Failures: 0, Errors: 0, Skipped: 3
  [INFO] BUILD SUCCESS / Total time: 04:59 min
  Phase 12 基线 371 → 391(净 +20)。

报告:docs/Phase-13-V11-Recovery-Completion-Report-2026-09-16.md
Phase 13A 未自动进入,等待用户确认。
2026-09-17 00:01:53 +08:00

270 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 13 V1.1 Recovery Completion Report — 公益红娘转介
> 日期:2026-09-16
> 阶段:Phase 13(Matchmaker Referral — KEEP-VERIFY/EXTEND)
> 分支:`main` | Tag:`phase13-recovery-2026-09-16`
> 结论:**CLOSED(Gate 全项通过)**
---
## 一、计划目标与 Gate
计划出处:`docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md` 第 256-264 行。
| 计划项 | 要求 | 结论 |
|---|---|---|
| 计划项 1 | 保留红娘服务和人工推荐;验证任何工作人员不能替用户 Like | ✅ 通过(替换假测试,6 用例行为验证) |
| 计划项 2 | 补齐转介咨询、活动、服务站的用户同意、来源追踪和状态记录 | ✅ 通过(6 用例,真实落库校验) |
| 计划项 3 | 红娘只能看转介状态和必要摘要,不能读取 counseling followup 正文 | ✅ 通过(3 用例 + 越权防护) |
**Gate:** 转介形成可追踪记录但不越权;用户决定权测试通过。
| Gate 断言 | 证据 | 结果 |
|---|---|---|
| 转介形成可追踪记录 | `referToActivityCreatesTrackableRecord` / `referToServiceStationCreatesTrackableRecord`:`source=matchmaker_referral`、`requestSource` 同名、`createdByOperatorId` 落库 | ✅ |
| 转介不越权(用户决定权) | `otherMatchmakerCannotReferUser` / `otherUserCannotConfirmReferral` / `nonReferralCannotBeConfirmedAsReferral` | ✅ |
| 红娘不可替用户 Like | `matchmakerRecommendationDoesNotCreateLike` / `matchmakerCannotAcceptOnBehalfOfUser` / `onlyUserCanLikeViaRecommendation` | ✅ |
| 只读摘要不读正文 | `nonOwnerSeesSanitizedDetail` / `matchmakerDetailNeverExposesCounselingFollowupContent` | ✅ |
---
## 二、复验发现:历史证据全为假测试
Phase 13 历史文档声称「**INV-005 红娘不可代用户操作验证 ✅**」。复验发现该结论**没有任何真实证据支撑**:
| 声称的验证 | 实际情况 |
|---|---|
| `MatchmakerServiceTest#testMatchmakerCannotLike` | `assertTrue(true); // 占位测试` —— 零覆盖 |
| `CoreInvariantsTest#testMatchmakerCannotLike` | 断言 `MATCHMAKER_STATUS_SUBMITTED/ASSIGNED/IN_SERVICE/COMPLETED` 四个**状态常量字符串**,与「红娘不能替用户 Like」**毫无关系**(名不副实的假测试) |
| 测试库覆盖 | `schema-h2.sql` **完全缺失** `matchmaker_services` / `matchmaker_recommendations` / `matchmaker_followups` 三表 → 该模块**零真实行为覆盖** |
另发现同类:`CoreInvariantsTest#testCrossOrgNoAccess`(INV-010)也是 `assertTrue(true)`。
---
## 三、缺陷清单(D1~D7)
### D1 — Critical(生产故障):推荐状态 CHECK 约束与代码写入值漂移
- **DDL**:`V009__add_matchmaker_tables.sql:55-57` 的 `chk_rec_status` 只允许
`pending_view` / `viewed` / `responded`,且**全迁移目录无任何 ALTER/DROP 扩展它**。
- **代码**:`MatchmakerService.java:225/250` 写入 `liked` / `passed` / `confirmed_mutual`。
- **后果**:真实 PostgreSQL 上用户点击「接受/跳过」红娘推荐 → 违反 CHECK → **必然 500**。Phase 13 主流程不可用。
- **修复**:`V038` 用 `DROP CONSTRAINT IF EXISTS` + 重加白名单(含 `liked`/`passed`/`confirmed_mutual`)。
### D2 — Critical(生产故障):service_type 缺省兜底值越界
- **DDL**:`V009:26-29` 的 `chk_matchmaker_service_type` 只允许
`recommend_partner` / `review_preference` / `contact_intention` / `join_activity` /
`no_match_help` / `chat_help` / `other`。
- **代码**:`MatchmakerController.java:47-48` 在请求未带 `serviceType` 时兜底写 `"recommendation"`;
`MatchmakerService.java:57-61` 注释声明 `referral_counseling` / `referral_activity` / `referral_station`。
- **后果**:真实 PG 上「提交红娘服务」(缺省路径)与三种转介子类型**必然 500**。
- **修复**:`V038` 扩展白名单纳入这 4 个值。
### D3 — High:`recommendPartner` 缺归属校验(越权推荐)
`MatchmakerServiceService.recommendPartner` 只校验 `service != null`,**无** `assigned_operator_id` 校验
→ 任何持 `matchmaker.service` 权限的 operator 可对**任意** serviceId 创建推荐。
(对比 `MatchmakerService.createRecommendation:118-121` 有校验 —— 同项目双路径口径不一致。)
**修复**:补归属校验 + 「不得推荐用户给自己」。
### D4 — High:`getServiceDetail` 无归属校验(越权读)
任何持 `matchmaker.view` 的 operator 可读**任意**服务详情,含完整用户资料
(displayName / gender / age / regionName / education / occupation / maritalStatus)。
**修复**:改为按调用者授权范围返回 —— 负责人得完整详情;非负责人得脱敏视图
(无 `userProfile`、无 `userId`、无 followup 正文,仅 `followupCount` 摘要)。
### D5 — High:转介无授权校验(越权代操作)
`ReferralService.referToActivity` / `referToServiceStation` 的 `userId` **直接取自请求体**,
`ReferralService` 甚至**未注入** `MatchmakerServiceMapper` —— 零授权校验。
任何红娘可把**任意用户**转介到任意活动/服务站,并顺带为该用户创建
`activity_registrations` / `service_appointments` 记录。
**修复**:新增 `assertMatchmakerServesUser`,断言存在进行中的
`matchmaker_service(assigned_operator_id=红娘, user_id=目标用户)`,否则拒绝。
### D6 — High(生产故障):`MatchmakerFollowupMapper.findByServiceId` 缺 `@Select`
```java
// 修复前 —— 只有声明,MyBatis 无法绑定
List<MatchmakerFollowup> findByServiceId(String serviceId);
```
运行时抛 `Invalid bound statement (not found):
...MatchmakerFollowupMapper.findByServiceId`。调用方
`MatchmakerServiceService.getServiceDetail:311` 因此**必然 500** ——
红娘工作台「查看服务详情」完全不可用。
此前不可见:测试库缺 `matchmaker_followups` 表,无任何测试能走到这条路径。
**修复**:补 `@Select` 注解(含 `deleted_at IS NULL` 过滤 + `@Param`)。
### D7 — Medium:Admin referral 前端契约缺口
| admin-web 调用 | 后端实际 | 结果 |
|---|---|---|
| `GET /referrals/my-history`(ReferralManagementView) | 该端点用 `UserPrincipal`(C 端) | 运营端无"我的用户身份" |
| `GET /matchmaker/services/operator/queue` | `GET /operator/matchmaker/pending` | 404 |
| `GET /matchmaker/recommendations/operator` | **不存在** | 404 |
| `PUT /matchmaker/services/{id}/assign` | `POST /operator/matchmaker/services/{id}/assign` | 404(方法+前缀均不符) |
| `POST /matchmaker/services/{id}/recommendation` | `POST /operator/matchmaker/services/{id}/recommend` | 404(路径+前缀不符) |
**修复**:后端新增 `GET /referrals/operator/history`(按 `created_by_operator_id` 过滤)
与 `GET /operator/matchmaker/recommendations`(限本人名下服务);前端 5 处调用改为真实路径。
---
## 四、测试保真度
### 假测试清理
| 文件 | 问题 | 处理 |
|---|---|---|
| `MatchmakerServiceTest` | 含 `assertTrue(true); // 占位测试` | 重写为**直接解析迁移 SQL** 的口径守卫(6 用例) |
| `CoreInvariantsTest#testMatchmakerCannotLike` | DisplayName 称"红娘不能替用户 Like",实际断言 `MATCHMAKER_STATUS_*` 常量 | DisplayName 如实改为"常量口径 INV-007: 红娘服务状态取值固定",并指向真实验证位置 |
| `CoreInvariantsTest#testCrossOrgNoAccess` | `assertTrue(true)` | 改为有意义的常量断言 + 注明行为验证待 Phase 19 |
| `CoreInvariantsTest` 类头 | —— | 新增注释说明本类**只做常量口径守卫,不验证行为**,并列出各 INV 的真实验证位置 |
### 新增真实行为测试 `MatchmakerReferralPhase13Test`(17 用例)
走**真实服务方法 + 真实落库**(`@SpringBootTest` + `@Transactional`):
| 分组 | 用例 |
|---|---|
| 计划项 1(红娘不代 Like) | `matchmakerRecommendationDoesNotCreateLike`、`onlyUserCanLikeViaRecommendation`、`matchmakerCannotAcceptOnBehalfOfUser`、`userPassViaRecommendation` |
| 计划项 2(同意/来源/状态) | `referToActivityCreatesTrackableRecord`、`referToServiceStationCreatesTrackableRecord`、`userConfirmsActivityReferral`、`userConfirmsServiceStationReferral`、`otherUserCannotConfirmReferral`、`nonReferralCannotBeConfirmedAsReferral` |
| 越权防护(D3/D5) | `otherMatchmakerCannotReferUser`、`otherMatchmakerCannotRecommend`、`ownerMatchmakerCanRecommend`、`cannotRecommendUserToSelf` |
| 计划项 3(只读摘要) | `nonOwnerSeesSanitizedDetail`、`ownerSeesFullDetail`、`matchmakerDetailNeverExposesCounselingFollowupContent` |
> 注:`createUser` 会补齐 `user_profiles`(approved)+ `user_identities`(verified),
> 使 `capabilityService.isBaseEligible` 通过,从而让「用户接受推荐 → 产生 liked」这条
> 真实路径可以走通(第一版测试因缺这两张表而假红)。
### 测试库补齐 `matchmaker_*` 三表
`schema-h2.sql` 新增三表,**约束与 V038 对齐**(含扩展后的 `chk_rec_status` /
`chk_matchmaker_service_type`)。同步从 `EntityMigrationParityTest.TEST_SCHEMA_OPTIONAL_TABLES`
白名单移除这三张表,并加入 `REQUIRED_IN_TEST_SCHEMA`——守卫从此**强制**它们必须存在。
### 两轮负向验证(守卫有效性证明)
**注入缺陷**:把 `chk_rec_status` 回退到 V009 原白名单(DDL + 测试库同步收窄)。
**第一轮 —— 暴露守卫缺口:**
| 守卫 | 结果 |
|---|---|
| `MatchmakerReferralPhase13Test.onlyUserCanLikeViaRecommendation` | ✅ `DataIntegrityViolation`(精准变红) |
| `MatchmakerReferralPhase13Test.userPassViaRecommendation` | ✅ `DataIntegrityViolation` |
| `MatchmakerServiceTest`(**抄副本版**常量守卫) | ❌ **仍然全绿** —— 它比对的是一份与 DDL 脱钩的常量副本,对漂移无反应 |
**修正**:把常量守卫改为**直接解析 `db/migration/V*.sql`**,按文件名顺序处理
`DROP CONSTRAINT IF EXISTS` + `ADD CONSTRAINT`,取"最终有效"白名单(解析器另有自检用例
`parserMustActuallyResolveConstraint` 防止守卫因解析失效而永真)。
**第二轮 —— 守卫全部精准变红:**
```
[ERROR] MatchmakerServiceTest.recommendationStatusConstantsAreWithinProductionWhitelist:112
Multiple Failures (3 failures)
[ERROR] MatchmakerReferralPhase13Test.onlyUserCanLikeViaRecommendation:149 » DataIntegrityViolation
[ERROR] MatchmakerReferralPhase13Test.userPassViaRecommendation:178 » DataIntegrityViolation
Tests run: 23, Failures: 1, Errors: 2
```
还原缺陷后 6/6 + 17/17 恢复全绿。
---
## 五、测试结果
### 定向测试(还原缺陷后)
| 测试类 | 结果 |
|---|---|
| `MatchmakerReferralPhase13Test` | 17/17 ✅ |
| `MatchmakerServiceTest` | 6/6 ✅ |
| `CoreInvariantsTest` | 12/12 ✅ |
| `MigrationValidationTest` | 12/12 ✅ |
| `EntityMigrationParityTest` | 5/5 ✅ |
| **合计** | **51 tests / 0 failures / 0 errors** |
### 全量回归
离线容器(`danyang-match-backend-test:latest`)实测,`mvn -o test` 汇总行:
```
[WARNING] Tests run: 391, Failures: 0, Errors: 0, Skipped: 3
[INFO] BUILD SUCCESS
[INFO] Total time: 04:59 min
```
| 项 | 值 |
|---|---|
| 用例总数 | **391**(Phase 12 基线 371 → +20) |
| Failures / Errors | **0 / 0** |
| Skipped | 3 |
| 结果 | **BUILD SUCCESS**(4 分 59 秒) |
> **计数说明(沿用 AGENTS.md 纪律:以实测为准,不预估)**:本轮新增 23 例(`MatchmakerReferralPhase13Test` 17 +
> `MatchmakerServiceTest` 重写后 6),同时删除 3 处占位断言,净增 **+20**(371 → 391)。
> 汇总行是 `[WARNING]` 而非 `[INFO]`(因含 skipped 用例),grep 时不可只匹配 `^\[INFO\]`。
---
## 六、变更文件清单
| 文件 | 类型 |
|---|---|
| `backend/src/main/resources/db/migration/V038__align_matchmaker_status_constraints.sql` | 新增 |
| `backend/src/test/java/.../matchmaker/MatchmakerReferralPhase13Test.java` | 新增(17 用例) |
| `backend/src/test/java/.../matchmaker/MatchmakerServiceTest.java` | **重写**(删假测试,改为解析 DDL 的守卫) |
| `backend/src/test/java/.../invariants/CoreInvariantsTest.java` | 修改(假测试如实改名) |
| `backend/src/test/resources/schema-h2.sql` | 修改(补 `matchmaker_*` 三表) |
| `backend/src/main/java/.../matchmaker/mapper/MatchmakerFollowupMapper.java` | 修改(D6:补 `@Select`) |
| `backend/src/main/java/.../matchmaker/mapper/MatchmakerRecommendationMapper.java` | 修改(新增 `findByOperatorId`) |
| `backend/src/main/java/.../matchmaker/mapper/MatchmakerServiceMapper.java` | 修改(新增 `findActiveByOperatorAndUser`) |
| `backend/src/main/java/.../matchmaker/service/MatchmakerServiceService.java` | 修改(D3/D4) |
| `backend/src/main/java/.../matchmaker/controller/MatchmakerOperatorController.java` | 修改(D4/D7) |
| `backend/src/main/java/.../referral/service/ReferralService.java` | 修改(D5/D7) |
| `backend/src/main/java/.../referral/controller/ReferralController.java` | 修改(D7 新增运营端端点) |
| `admin-web/src/modules/matchmaker/MatchmakerManagementView.vue` | 修改(D7 契约修正) |
| `admin-web/src/modules/referral/ReferralManagementView.vue` | 修改(D7 契约修正) |
| `miniapp/services/referralService.js` | 新增(转介确认客户端) |
| `backend/src/test/java/.../deployment/EntityMigrationParityTest.java` | 修改(白名单迁移) |
| `backend/src/test/java/.../deployment/MigrationValidationTest.java` | 修改(`LATEST_MIGRATION` 37→38) |
| `docs/STATUS.md`、`docs/JIRA_BACKLOG.md`、本报告 | 文档 |
---
## 七、遗留与 gap(登记,不自动实现)
| 项 | 说明 |
|---|---|
| **miniapp 转介确认 UI 缺页** | 已补 `referralService.js`(客户端 API 层),但小程序**尚未有页面**承载"我的转介 → 确认"交互。属**新功能开发**(非本轮 verify-extend 范围内),建议单列任务。当前用户只能通过 API 层确认,无 UI 入口。 |
| `miniapp` 服务详情端点缺失 | `matchmakerService.getServiceDetail` 指向 `GET /matchmaker/services/{id}`(用户端),后端**无此端点**(operator 侧才有 `/operator/matchmaker/services/{id}`)。`matchmaker-progress` 页面依赖它。 |
| `matchmaker-service-detail` 页面不存在 | `matchmaker-workbench.js:77` 跳转该页,目录不存在。 |
| `ReferralController.referToServiceStation` 的 `preferredDate` | 占位实现(`Instant.now()`),未解析日期字符串 —— 代码内已有 `// placeholder` 注释。 |
| `matchmaker_services` 的 `referral_target_type/id`、`referral_source_type` | DDL 有列,Java 侧**无写入**(死列)。 |
| `MatchmakerService.createReferral` | 返回内存 record、**不落库**,且无 Controller 端点调用(**死代码**)。真正的转介落库走 `ReferralService`。 |
| `CounselingServiceApp` | **孤儿类**(无任何注入点)。其 `addFollowup` → `toFollowupDTO` 会把 counseling followup 正文**明文回传**,但当前**无 HTTP 端点可达**。计划项 3 目前"因功能缺失而侥幸成立"。**若 Phase 13A 接通该端点,必须同时加脱敏**。 |
| counseling 模块零 `@PreAuthorize` | 权限全靠 Service 层手工判(`getDetail` 的 role 分支)。建议 Phase 13A 统一收口。 |
| `TEST_SCHEMA_OPTIONAL_TABLES` 仍有 30 项 | 每补齐一张应移除一项;`matchmaker_*` 本轮已移除。 |
---
## 八、结论
Phase 13 三个计划项与 Gate 全项通过。全量回归 **391 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**。
本轮最重要的发现是 **D1/D2/D6 三处「只在真实 PostgreSQL 上炸」的生产故障**,其共同隐蔽机制是:
测试库 `schema-h2.sql` 与生产迁移漂移(缺表 + 缺约束)→ **测试绿而生产崩**。
这与 Phase 12 发现的 D1 是**同族根因**,本轮把守卫进一步收紧为「直接解析迁移 SQL」。
**PHASE 13 CLOSED. Phase 13A 未自动进入,等待用户确认。**