KEEP-VERIFY 复验发现并修复 4 处真实缺陷: - D1(Critical,生产故障·同一族系统性缺陷)实体与 db/migration 系统性漂移:23 表 50 列在生产 迁移中不存在,而测试库 schema-h2.sql 手写补齐 → 全量绿=假绿。其中 11 表属"INSERT 必崩"级 (缺 id/created_at/updated_at,被 AutoFillHandler 强制填充),含 matches.episode、user_preferences 权重列、permissions/roles、consent_records、risk_events 等;真实 PG 上 Match 创建、择偶偏好保存、 权限/角色写入、同意记录、风险事件全部失败。修复:新增 V037(幂等 ADD COLUMN IF NOT EXISTS + 存量 episode 回填)。 - D2(High)RelationshipService.createMatch 不写 episode/lastActivityAt,与 MatchService 双路径 填充不一致;且 episode 为 null 时 m.getEpisode()+1 对 Integer 自动拆箱抛 NPE。修复:统一按 AGENTS.md #20 推导 + null 兜底。 - D3(High)测试库缺 user_identities 等 7 表 → GET /api/v1/home 在测试环境 BadSqlGrammar, 首页接口零回归覆盖。修复:schema-h2.sql 补齐 7 表。 - D4(Medium)Phase 12 计划项 2 实际未实现:getting_to_know 分支只产出 counseling。修复: HomeApplicationService 补 premarital 引导(与 counseling 并列)。 测试保真度: - 新增永久守卫 EntityMigrationParityTest(5 用例):把"实体字段 ⊆ 生产迁移累积列"与"测试库覆盖 关键业务表"固化为自动不变量,区分"必崩级/潜伏级";经两轮负向验证证明会变红(第一轮分级过窄 已修正:episode 由"潜伏级"纠正为"必崩级")。 - 新增 RelationshipProgressPhase12Test(14 用例,含 8 线程并发确认)与 HomePremaritalGuidancePhase12Test(5 用例)。 - 删除 2 个假测试类中的 assertTrue(true) 占位(AGENTS.md 明令禁止),改走真实服务方法验证。 Gate:单方确认不改变最终关系 ✅ / 双确认后才 successful ✅ / 历史 Episode 可追踪 ✅ / 新 Episode 不恢复旧聊天权限(conversations.match_id UNIQUE)✅ / 非成员不可代确认 ✅ 全量回归:371 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(71 类,5m07s)
14 KiB
Phase 12 V1.1 Recovery Completion Report — Relationship Progress
日期:2026-09-16 阶段:Phase 12(Relationship Progress — KEEP-VERIFY/EXTEND) 分支:
main| Tag:phase12-recovery-2026-09-16结论:CLOSED(Gate 全项通过),累计全量回归 371 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS
一、计划目标与 Gate
计划出处:docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md 第 238-246 行。
| 计划项 | 要求 | 结论 |
|---|---|---|
| 计划项 1 | 验证成功关系双确认、恢复/退出、历史 Episode 和并发确认 | ✅ 通过(14 用例行为验证) |
| 计划项 2 | 补充 getting_to_know 阶段的婚前辅导入口引导 |
✅ 原未实现,本轮补实现 |
| 计划项 3 | 验证工作人员不能替用户确认成功关系(INV-005) | ✅ 通过(非成员路径断言) |
Gate: 单方确认不改变最终关系;历史 Episode 可追踪但不会恢复旧聊天权限。
| Gate 断言 | 证据 | 结果 |
|---|---|---|
| 单方确认不改变最终关系 | singleConfirmDoesNotCompleteMatch:A 单方确认后 Match 仍非 successful、success request 仍 pending |
✅ |
| 双确认后才 successful | bothConfirmCompletesMatch:A 确认 → B 确认 → Match = successful |
✅ |
| 历史 Episode 可追踪 | reMatchIncrementsEpisode:先 unmatch 后重建,episode 1 → 2 |
✅ |
| 新 Episode 不恢复旧聊天权限 | reMatchDoesNotRestoreOldConversation:新 Match 零会话;旧会话 match_id 仍指向旧 Match(conversations.match_id UNIQUE 约束) |
✅ |
| 工作人员不能代确认 | nonMemberCannotConfirm:非成员 userId 确认被业务异常拒绝 |
✅ |
二、复验方法论(KEEP-VERIFY / EXTEND)
沿用前四阶段已确立的复验纪律:历史 ✅ 不可信,「测试覆盖了函数」≠「测试覆盖了运行时路径」。逐条核对运行时路径与生产约束,发现真实缺陷则修复,并补齐测试保真度。
本轮新增第三层保真度要求并固化为守卫:
- 生产 DDL 约束必须下沉测试库(Phase 08~11 已建立);
- 测试必须走真实方法路径,不得传字面量绕过服务层;
- 测试库 schema 必须与生产迁移对齐 ← 本轮新发现的最大盲区(见 D1)。
三、缺陷清单(D1~D4)
D1 — Critical(生产故障 · 同一族系统性缺陷):实体与生产迁移系统性漂移
现象: 23 张表、50 个列在 backend/src/main/resources/db/migration/V*.sql 中根本不存在,但实体(@TableName)声明了这些字段;同时测试库 backend/src/test/resources/schema-h2.sql 是手写最小 schema,长期比生产迁移多出这些列。
后果: H2 测试库有列 → 全量测试绿;真实 PostgreSQL 无列 → 运行时 column "xxx" of relation "yyy" does not exist。全量测试绿 = 假绿。
分级(依据 MyBatis-Plus 字段策略):
- MyBatis-Plus 默认
FieldStrategy=NOT_NULL→ null 字段不进 SQL 列清单; AutoFillHandler.insertFill强制填充id/createdAt/updatedAt→ 这些字段必然出现在 INSERT;- 因此缺
id/created_at/updated_at的表属**"INSERT 必崩"级**。
"必崩级"(11 张表):
| 表 | 缺失列 | 业务影响 |
|---|---|---|
matches |
episode、source_id、last_activity_at、unmatched_by、deleted_at |
Match 创建必崩;episode 追踪不可用 |
user_preferences |
id + 11 个权重列 + deleted_at |
择偶偏好保存必崩 |
permissions、roles |
created_at/updated_at/deleted_at |
权限/角色写入必崩 |
consent_records |
created_at/updated_at/deleted_at |
同意记录必崩 |
risk_events、review_reasons |
id/created_at/updated_at |
风险事件/审核理由写入必崩 |
match_rule_configs、matchmaker_followups、matchmaker_recommendations |
updated_at/deleted_at |
红娘服务写入必崩 |
user_privacy_settings |
id/deleted_at |
隐私设置写入必崩 |
"潜伏级"(其余 12 张表): 缺 deleted_at 等,null 不进 SQL,但一旦写入非 null 值即崩。含 local_eligibility_evidences、service_program_members、user_local_eligibilities、user_relations、user_identities、organization_relationships、activity_capacity_groups、activity_mutual_connection_consents、activity_organizations、matchmaker_services、review_items、review_tasks。
修复: 新增 backend/src/main/resources/db/migration/V037__align_entity_columns_with_migrations.sql(165 行):
- 全部采用
ADD COLUMN IF NOT EXISTS(幂等,可重复执行); review_reasons/user_preferences/user_privacy_settings等 PK 非id的表补id列 + 唯一索引;matches.episode INT NOT NULL DEFAULT 1+ 存量数据回填UPDATE matches SET episode = 1 WHERE episode IS NULL;- 补两个部分索引:
idx_matches_episode、idx_matches_deleted。
D2 — High:双路径字段填充不一致 + Integer 自动拆箱 NPE
现象:
RelationshipService.createMatch创建 Match 时不写episode/lastActivityAt;MatchService.createOrReuseMatch写了;- 两条路径殊途同归到同一张表 → 字段填充不一致;
- 且
MatchService中m.getEpisode() + 1在episode为 null 时对Integer自动拆箱抛 NPE。
修复:
- 两条路径统一按 AGENTS.md #20 语义推导 episode:
findLatestMatch→ 若 prior 为unmatched则prior.episode + 1,否则 1; - 补 null 兜底:
(priorEpisode == null ? 1 : priorEpisode) + 1; - 两处均设
lastActivityAt = Instant.now()。
涉及文件: relationship/service/RelationshipService.java、match/service/MatchService.java
D3 — High:测试库缺 7 张表,首页接口零回归覆盖
现象: 测试库 schema-h2.sql 缺 user_identities / review_tasks / local_eligibility_evidences / home_banners / service_locations / content_articles / activities。GET /api/v1/home 在测试环境直接抛 BadSqlGrammar("Table USER_IDENTITIES not found")。
后果: 首页接口在测试环境根本无法执行 → 所谓"首页已通过"零回归覆盖。
修复: backend/src/test/resources/schema-h2.sql 补齐 7 张表(列对齐生产迁移)。
D4 — Medium:Phase 12 计划项 2 实际未实现
现象: HomeApplicationService.buildServiceRecommendations 的 getting_to_know 分支只产出 counseling;premarital(婚前辅导)入口仅在 matched_successfully 才出现。
修复: getting_to_know 分支补 premarital 引导,与 counseling 并列(不互相挤占):
} else if (Constants.RELATIONSHIP_STATUS_GETTING_TO_KNOW.equals(status)) {
list.add(serviceRec("counseling", ...));
// Phase 12(计划项 2):getting_to_know 阶段补充婚前辅导入口引导。
list.add(serviceRec("premarital", "想更认真地了解彼此?",
"善缘丹阳公益婚前辅导帮助你们在决定前更好地了解彼此。",
"了解婚前辅导", "/pages/premarital-intro/premarital-intro"));
}
四、新增永久守卫:EntityMigrationParityTest
backend/src/test/java/com/danyang/match/platform/deployment/EntityMigrationParityTest.java(5 用例)
把「实体字段 ⊆ 生产迁移累积列」与「测试库覆盖关键业务表」固化为编译后自动可验证的不变量:
| 用例 | 作用 |
|---|---|
allEntityFieldsExistInMigrations() |
文本级解析 db/migration/V*.sql 的 CREATE TABLE + ALTER ADD/DROP/RENAME COLUMN,对比 entity/*.java 的 @TableName 字段(含 camelToSnake、BaseEntity 继承列、@TableField(exist=false) 排除),区分硬失败/软失败,并标注必崩级/潜伏级 |
parserMustActuallySeeColumns() |
解析器自检(防止守卫因解析失效而永远为真) |
entityParserMustSeeBaseEntityColumns() |
同上,验证 BaseEntity 继承列被识别 |
testSchemaCoversCriticalTables() |
断言 REQUIRED_IN_TEST_SCHEMA(14 张关键表)都在 schema-h2.sql |
testSchemaMissingTablesAreWhitelisted() |
生产表缺于测试库时必须登记在 TEST_SCHEMA_OPTIONAL_TABLES(33 项白名单) |
关键常量:
private static final Set<String> AUTO_FILLED_COLUMNS = Set.of("id", "created_at", "updated_at");
private static final Set<String> ALWAYS_WRITTEN_COLUMNS = Set.of(
"matches.episode", "matches.last_activity_at", "user_preferences.deleted_at");
负向验证(守卫有效性证明)
第一轮: 注入缺陷(从 V037 移除 matches.episode 补齐)→ allEntityFieldsExistInMigrations + parserMustActuallySeeColumns 变红,但分级误标为"潜伏级"。
修正: 新增 ALWAYS_WRITTEN_COLUMNS(业务代码每路径必 set 的列),episode 由"潜伏级"纠正为"必崩级"。
第二轮: 再次注入同一缺陷 → 守卫精准报出:
【必崩级】matches: 生产缺【必崩】列 [episode]
→ 守卫有效性得证。
五、新增行为测试
RelationshipProgressPhase12Test(14 用例,@SpringBootTest + @ActiveProfiles("test"))
| 用例 | 验证点 |
|---|---|
singleConfirmDoesNotCompleteMatch |
Gate 前半:单方确认不改变最终关系 |
bothConfirmCompletesMatch |
双确认 → successful |
successStopsRecommendations |
成功后推荐停止 |
samePersonCannotConfirmTwice |
同一人重复确认被拒 |
concurrentConfirmIsIdempotent |
8 线程 CountDownLatch 并发确认 → Match 仅 successful 一次、success request 仅 1 条 |
reMatchIncrementsEpisode |
episode 1 → 2 |
reMatchDoesNotRestoreOldConversation |
新 Match 零会话;旧会话仍属旧 Match |
declinedCannotRestart |
declined 不可重启 |
successfulCannotRestart |
successful 不可重启 |
pauseKeepsExistingMatch |
暂停保留既有 Match |
resumeRestoresSearching |
恢复回到 searching |
exitServiceWorks |
退出服务可用 |
nonMemberCannotConfirm |
INV-005:工作人员代操作被拒 |
confirmedByIsAlwaysTheActor |
confirmedBy 恒为发起者 |
HomePremaritalGuidancePhase12Test(5 用例)
| 用例 | 验证点 |
|---|---|
gettingToKnowProvidesPremaritalEntry |
计划项 2:getting_to_know 提供婚前辅导入口 |
gettingToKnowKeepsCounseling |
补 premarital 后 counseling 未被挤占 |
successfulKeepsPremaritalEntry |
successful 仍有婚前辅导入口 |
searchingHasNoPremarital |
searching 不应出现 |
allTargetsAreInternalPages |
所有入口指向站内页 |
六、删除假测试
| 文件 | 问题 | 处理 |
|---|---|---|
relationship/RelationshipStatusServiceTest.java |
含 2 条 assertTrue(true); // 占位测试 |
删除占位;改为 4 个对抗生产 CHECK 约束漂移的断言(matchStatus/matchSource/successRequestStatus 与 V003 白名单一致、关系状态取值互异) |
relationship/RelationshipStatusServiceDoubleConfirmTest.java |
含占位断言,对 INV-005 零覆盖 | 删除占位;改为口径守卫 + 注释指向 RelationshipProgressPhase12Test |
AGENTS.md 明令禁止假测试。行为验证统一移至走真实服务方法的
RelationshipProgressPhase12Test。
七、测试结果
定向测试
| 测试类 | 结果 |
|---|---|
EntityMigrationParityTest |
5/5 ✅(修白名单后单独复跑 5/5) |
MigrationValidationTest |
12/12 ✅ |
HomePremaritalGuidancePhase12Test |
5/5 ✅ |
RelationshipProgressPhase12Test |
14/14 ✅ |
RelationshipStatusServiceDoubleConfirmTest |
5/5 ✅ |
RelationshipStatusServiceTest |
7/7 ✅ |
全量回归
371 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS
(71 个测试类,耗时 5m07s)
计数对账: Phase 11 基线 346 → 本轮 371,净增 25。
| 增量来源 | 用例数 |
|---|---|
新增 RelationshipProgressPhase12Test |
+14 |
新增 EntityMigrationParityTest |
+5 |
新增 HomePremaritalGuidancePhase12Test |
+5 |
删除 assertTrue(true) 占位后为两个 relationship 测试类补齐的真实断言(原计数方式不同) |
+1 |
| 合计 | +25 |
⚠️ 规划阶段曾预估 358(按"仅新增 3 个测试类共 24 例"粗算),实测 371。以实测日志为准,
STATUS.md/JIRA_BACKLOG.md已同步修正。
八、变更文件清单
| 文件 | 类型 |
|---|---|
backend/src/main/resources/db/migration/V037__align_entity_columns_with_migrations.sql |
新增(165 行) |
backend/src/test/java/.../deployment/EntityMigrationParityTest.java |
新增 |
backend/src/test/java/.../relationship/RelationshipProgressPhase12Test.java |
新增 |
backend/src/test/java/.../home/HomePremaritalGuidancePhase12Test.java |
新增 |
backend/src/main/java/.../home/service/HomeApplicationService.java |
修改(D4) |
backend/src/main/java/.../relationship/service/RelationshipService.java |
修改(D2) |
backend/src/main/java/.../match/service/MatchService.java |
修改(D2) |
backend/src/test/resources/schema-h2.sql |
修改(D3,补 7 表) |
backend/src/test/java/.../deployment/MigrationValidationTest.java |
修改(LATEST_MIGRATION 36→37) |
backend/src/test/java/.../relationship/RelationshipStatusServiceTest.java |
重写(删假测试) |
backend/src/test/java/.../relationship/RelationshipStatusServiceDoubleConfirmTest.java |
重写(删假测试) |
docs/STATUS.md、docs/JIRA_BACKLOG.md、本报告 |
文档 |
九、遗留与后续
| 项 | 说明 |
|---|---|
schema-h2.sql 手写维护风险 |
本轮已通过 EntityMigrationParityTest 守卫;长期建议改为由 Flyway 迁移生成测试 schema(或用 Testcontainers 直连生产迁移),消除手写漂移根源 |
TEST_SCHEMA_OPTIONAL_TABLES 白名单 |
现有 33 项;后续新增表需同步登记,否则守卫变红(这是设计意图) |
| Phase 13(公益红娘转介 — EXTEND) | 未自动进入,等待用户确认 |
十、结论
Phase 12 三个计划项与 Gate 全项通过。其中 D1 为跨阶段共性根因——前四阶段(Phase 08~11)之所以能看到"测试绿而生产崩",正是因为测试库 schema 手写且与生产迁移漂移;本轮不仅修复了当前缺陷,还以 EntityMigrationParityTest 把这一族问题固化为自动守卫。
PHASE 12 CLOSED.