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 未自动进入,等待用户确认。
16 KiB
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
// 修复前 —— 只有声明,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 例(
MatchmakerReferralPhase13Test17 +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 未自动进入,等待用户确认。