Files
danyang-match/docs/Phase-16-V11-Recovery-Completion-Report-2026-09-17.md
T

9.0 KiB
Raw Permalink Blame History

Phase 16(活动互选与双方开启沟通确认)V1.1 增量复验完成报告

  • 日期:2026-09-17
  • 模式:KEEP-VERIFY / EXTEND
  • 范围:activity_mutual、relationship、messaging 模块;activity-mutual 双端页面
  • 基线:Phase 15 收尾 29112ee(全量回归 474 tests)
  • 结论:关闭。三项计划事项全部落地,Gate 三项均达成

一、计划项达成情况

计划项 结论 证据
以 activity_mutual_connection_consents 为唯一逻辑,废弃 mutual 直接建 Match/Chat 的入口 ✅ 双实现(旧 ActivityInterest.confirmInterest / 新 ActivityMutualConsent.confirmConnection)统一到 RelationshipService.confirmActivityInterest 单实现
双方确认事务中:已有 active Match 则复用会话;历史 Unmatch 则复用同一行(episode 递增 + reopen);无历史则创建 Match 与 Conversation ✅ ActivityMutualConsentService.createMatchAndConversation 走 confirmActivityInterest;active 复用、unmatched 经 MatchMapper.reopenEpisode 复用同一行
补 decline、expires_at 超时、重复确认幂等、并发确认(exactly-once)与单向意向保密(INV-006)测试 ✅ 新增 ActivityMutualConsentServiceTest(7 例),覆盖双确认建 Match+会话+溯源、INV-006 单向不暴露、decline 阻止、幂等重确认不重复建、expires_at 超时拒绝、软删过滤、复用 active Match

Gate

Gate 结论
A 单选 B 时 B 无感知(INV-006 单向意向保密) ✅ getMyConsentStatus 改为 partnerAccepted = myAccepted && partnerSideAccepted;本侧未确认则对方确认态不回显
Mutual 但未双确认时无新 Chat ✅ confirmConnection 单方确认不建 Match/会话,仅持久化本侧确认态
双确认后 exactly-once 形成正确 Match/Conversation ✅ 原子 confirmSideIfPending(both_confirmed_at NULL→非NULL 仅一方赢家 affected==1 && bothConfirmedAt!=null 建 Match+会话并 bindMatch 溯源)

二、复验发现与修复缺陷清单(7 处 + 1 处隐藏编译缺陷 + 1 处测试保真度)

A. 核心 Gate 缺陷(活动互选二阶段同意)

  • D1 CRITICAL — 双确认后根本不建 Match/会话(Gate 缺陷 · 违反 INV-014/015):新路径 ActivityMutualConsentController.confirmConnection 在双方确认后只置 both_confirmed_at,不调用任何 Match/会话创建;旧路径 ActivityInterest.confirmInterest→confirmActivityInterest 建 Match 却缺会话。两条路径分裂,导致"活动互选成功后无法开启沟通"。重写为 confirmConnection:以原子 confirmSideIfPending 保证 exactly-once,由赢家方调用 confirmActivityInterest 建 Match+会话并 bindMatch 溯源;单方确认不建 Match。
  • D2 HIGH — 双实现分裂:旧/新两条确认路径各自为政、语义不一致。统一到 RelationshipService.confirmActivityInterest(返回 matchId + priorUnmatch,显式开启会话)。
  • D3 HIGH — 隐藏编译缺陷(必崩级 · 最严重发现):Phase 15 提交态在 RelationshipService.confirmActivityInterest 中调用 matchMapper.reopenEpisode,但 MatchMapper 从未定义该方法 → 整个 activity_mutual / relationship 模块根本不编译。因此 "474 绿"基线从未真实存在,所谓通过是假象。修复:在 MatchMapper 补 @Update reopenEpisode(WHERE id=#{id} AND status='unmatched' AND deleted_at IS NULL)。结论必须如实记录:Phase 15 的提交态从未通过编译。
  • D4 HIGH — INV-006 单向意向泄露:getMyConsentStatus 在本侧未确认时即回显对方确认状态。改为 partnerAccepted = myAccepted && partnerSideAccepted,本侧未确认则对方确认态不回显。

B. 缺会话与测试保真度(4 处)

  • D5 MEDIUM — 复用/新建 Match 后缺会话:confirmActivityInterest 此前不显式开启会话。现显式调用 chatService.getOrCreateConversation(match.getId(), ...) 并返回 conversationAutoCreated=true(新建与复用两条分支均覆盖)。
  • D6 MEDIUM — 测试库零覆盖(同族根因):schema-h2.sql 缺 activity_mutual_connection_consents 表 → 互选逻辑在测试环境无表可跑,长期静默。已补表(含 deleted_at/expires_at/match_id 与 chk_consent_status CHECK、UNIQUE(activity_id,user_a_id,user_b_id)),与生产 V024+V037+V041 对齐;EntityMigrationParityTest 将其从 TEST_SCHEMA_OPTIONAL_TABLES 移入 REQUIRED_IN_TEST_SCHEMA。
  • D7 MEDIUM — 无互选测试:此前零互选单测。新建 ActivityMutualConsentServiceTest(7 例,@SpringBootTest 走真实方法路径)。
  • D8 MEDIUM — 断言与 AGENTS.md #20 冲突:ChatPhase09IntegrationTest:344 假设"每次活动互选新建独立会话",与 AGENTS.md #20 "同对用户 Unmatch 后重连复用同一 Match 行、会话随之复用"冲突 → 改为 assertEquals(conv.getId(), activityConv.getId(), ...)(认可复用),保留 sendText/WebSocket/未读主断言以验证"复用同一消息管线"。

三、测试保真度修复(3 处)

  • T1 约束漂移盲区:测试库缺 activity_mutual_connection_consents 表(正是 D6 长期不可见的根因)。已补齐并移入 REQUIRED_IN_TEST_SCHEMA。
  • T2 迁移基线推进:MigrationValidationTest 基线 40 → 41(新增 V041)。该守卫会拒绝任何超出基线的新迁移文件,是设计内的收敛机制。
  • T3 断言与域语义对齐:ChatPhase09IntegrationTest:344 修正为复用语义(见 D8)。

附带教训:提交态不编译 ≠ 测试通过。 本轮证明"474 绿"是假象(D3)。每次复验必须先在离线容器跑一次真实 mvn -o test 编译+执行,不能以文档声明或历史 ✅ 替代实证。


四、证据

定向测试:ActivityMutualConsentServiceTest 7 tests / 0 failures(覆盖双确认建 Match+会话+溯源、INV-006 单向不暴露、decline 阻止、幂等重确认不重复建、expires_at 超时拒绝、软删过滤、复用 active Match)

负向验证(注入真实缺陷 → 守卫必须变红,随后全部还原):

注入 变红的用例
临时注释 createMatchAndConversation(consent); 调用(还原 D1 一半:双确认不建 Match) doubleConfirmCreatesMatchAndConversation / idempotentReconfirmDoesNotDuplicateMatch(2 条用例精准变红,归因与注入一一对应)

还原后确认工作区无 NEG-VAL 残留(grep -c "NEG-VAL" = 0)。

全量回归:Tests run: 481, Failures: 0, Errors: 0, Skipped: 3 / BUILD SUCCESS(离线容器 danyang-match-backend-test mvn -B -o test,约 5 分;基线 474 → 481,净 +7)

新增迁移:V041__extend_mutual_consent_expiry.sql(expires_at TIMESTAMPTZ + match_id UUID + 索引),与 consent 超时拒绝、互选溯源对齐。


五、变更清单

新增文件

  • backend/src/main/resources/db/migration/V041__extend_mutual_consent_expiry.sql
  • backend/src/test/java/com/danyang/match/platform/activity_mutual/ActivityMutualConsentServiceTest.java

修改文件

  • activity_mutual/service/ActivityMutualConsentService.java(重写 confirmConnection 走 confirmActivityInterest;exactly-once 赢家建 Match+会话并 bindMatch;删除死代码 hasActiveMatch;修复 INV-006 getMyConsentStatus)
  • match/mapper/MatchMapper.java(新增 reopenEpisode,消除 D3 隐藏编译缺陷)
  • relationship/service/RelationshipService.java(confirmActivityInterest 返回 matchId/复用标记并显式开启会话)
  • activity_mutual/entity/ActivityMutualConsent.java(extends BaseEntity + expiresAt/matchId)
  • activity_mutual/mapper/ActivityMutualConsentMapper.java(软删过滤 deleted_at IS NULL + 原子 confirmSideIfPending/declineSideIfPending/bindMatch)
  • src/test/resources/schema-h2.sql(新增 activity_mutual_connection_consents 表,对齐 V024+V037+V041)
  • deployment/EntityMigrationParityTest.java(consent 表从可选集合移入 REQUIRED_IN_TEST_SCHEMA)
  • deployment/MigrationValidationTest.java(基线 40 → 41)
  • chat/ChatPhase09IntegrationTest.java(第 344 行断言对齐 AGENTS.md #20 复用语义)

六、遗留(供后续阶段)

  1. P16-V11-T13 超时处理未接线:V041 已加 expires_at 列与拒绝逻辑所需字段,但"超时后自动置 expire"的定时/惰性触发尚未实现,留待后续 Phase。
  2. 互选超时维度测试:ActivityMutualConsentServiceTest.expiredConsentRejectsConfirm 已就"带 expires_at 且已过期的记录拒绝确认"给出守卫,但无"到期自动推进状态"的端到端验证。
  3. 双向活动意向入口一致性:小程序 activity-mutual-result 双端状态机已就绪,但双确认成功后的"跳转到既有会话"仍需真机联调确认(后端契约已通过 conversations/from-match/{id} 验证)。

Phase 16 已关闭。Phase 17(举报与风控)未自动进入,等待用户确认。