Files
danyang-match/docs/Phase-21-V11-Recovery-Completion-Report-2026-09-18.md
T
WorkBuddy 0580da0c62 fix(account-lifecycle): close Phase 21 KEEP-VERIFY — 6 real defects + fake-green tests
KEEP-VERIFY re-audit of account lifecycle (EXTEND): STATUS marked ✅ but
contained hard Gate violations and dead code.

Real defects fixed:
- D-21-A: AuthService.checkAccountStatus did not reject CLOSED → closed
  account could still log in (Gate ❌→✅); added CLOSED branch before
  BANNED/SUSPENDED.
- D-21-B: notifyActiveMatches was a TODO stub → active Matches/conversations
  not closed on withdrawal (Gate ❌→✅); now finds active matches, sets
  UNMATCHED + closes conversation via chatService.closeConversationForMatch.
- D-21-C: cancelPendingActivityRegistrations was a TODO stub → future
  activity registrations not cancelled (Gate ❌→✅); now cancels pending/approved.
- D-21-D: anonymizeServiceHistory existed but was never wired into
  closeAccount → personal identity + counseling text leaked (Gate ❌→✅);
  wired in, anonymizes id_number/real_name with placeholder, plus
  counseling_followups content.
- D-21-E: closeAccount hard-threw on blockers → all cancelXxx unreachable
  (dead code contradiction); changed to informational log + continue cleanup.
- D-21-F: 7× assertTrue(true) fake-green tests → replaced with 11 real DB
  integration tests + 3 login-guard tests (Redis-decoupled via
  ReflectionTestUtils).

Cross-module defenses (RecommendationEngine/MatchEligibility/Capability
reject non-NORMAL) confirmed effective. Remaining (no silent scope creep):
matchmaker in-progress interception still hardcodes 0 (no table), data
retention delete flow is a product decision, privacy-settings endpoint stub.

Full regression: 650 tests / 0 failures / 0 errors / 3 skipped (baseline 648, +2).
2026-09-18 10:12:34 +08:00

86 lines
8.3 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 21(账户生命周期)KEEP-VERIFY 复验完成报告 — 2026-09-18
> **结论:CLOSED(Gate 全项通过)** —— 但这是**复验后**的结论,不是 `STATUS.md` 原有「✅」所声称的结论。
> 原有「✅」只是历史进度声明;KEEP-VERIFY 审计发现该模块同时存在 **Gate 硬违反、死代码/桩、以及一处会让"关闭未来服务"逻辑永远不可达的设计矛盾**。
---
## 一、复验起点:为什么「已完成」不可信
`STATUS.md` 早已把 Phase 21 标为「✅ 已完成」,P21-T01~T05 全部打勾。但 KEEP-VERIFY 实测发现:
- **Gate「注销后不能登录」被违反** —— `AuthService.checkAccountStatus` 只拒绝 `BANNED`/`SUSPENDED`,**不拒绝 `CLOSED`**,注销后仍能登录。
- **Gate「注销后不能聊天」被违反** —— `notifyActiveMatches` 是 **TODO 桩**(只写日志),注销后 `Match` 仍是 `active`、对方不被通知。
- **Gate「关闭未来服务」被违反** —— `cancelPendingActivityRegistrations` 是 **TODO 桩**,活动报名不取消。
- **Gate「不反向暴露个人和咨询正文」被违反** —— `anonymizeServiceHistory`(P21-T05 标的"✅")**实现了却从未被 `closeAccount` 调用**(死代码);且 `sanitizeUserData` 仅脱敏手机号,**身份证号脱敏是 TODO**,注销后身份证明文(密文)永久留存。
- **一处设计矛盾(死代码根因)** —— `closeAccount` 在 `checkCanWithdraw` 返回 `canWithdraw=false` 时**直接抛异常阻断**,而 `checkCanWithdraw` 检查的恰恰就是 `cancelXxx` 那些待取消的状态(预约/咨询/项目/报名/资格)。结论:下方"关闭未来服务"的清理逻辑**永远不可达**,且给用户注销设了"需先手动取消所有业务"的障碍——与 `AccountLifecycleService` 注释「注销必须在线便捷、不设置障碍」直接冲突。
- **测试假绿** —— `AccountLifecycleServiceTest` 由 **7 个 `assertTrue(true)`** 组成,零断言,却让"Phase 21 已完成"看起来成立。
**代码存在 ≠ 功能正确,更 ≠ Gate 通过。**
---
## 二、修复的真实缺陷(高价值项)
| 编号 | 严重度 | 缺陷 | 修复 |
|------|--------|------|------|
| **D-21-A** | 🔴 CRITICAL(Gate) | `AuthService.checkAccountStatus` 不拒绝 `CLOSED` → 注销后仍能登录 | 增加 `CLOSED` 拒绝分支(与 `BANNED`/`SUSPENDED` 同级) |
| **D-21-B** | 🟠 HIGH(Gate 暴露个人/咨询正文 + 死代码) | `anonymizeServiceHistory` 已实现但**未接线**;身份证脱敏是 TODO | `closeAccount` 接线 `anonymizeServiceHistory`;`sanitizeUserData` 增加 `user_identities` 身份证/姓名脱敏(列 `NOT NULL`,覆盖为占位符 `***ACCOUNT_CLOSED***`);`anonymizeServiceHistory` 扩展脱敏 `counseling_followups.content`(INV-013 保护的咨询正文) |
| **D-21-C** | 🟠 HIGH(Gate 关闭未来服务) | `cancelPendingActivityRegistrations` 是 TODO 桩 | 实现:全量取消用户 `pending`/`approved` 活动报名(用 `selectList` 规避既有 `findByUserId` 的 `LIMIT 20`) |
| **D-21-D** | 🟠 HIGH(Gate 注销后不能聊天) | `notifyActiveMatches` 是 TODO 桩 | 实现:查出用户 active/`getting_to_know` Match → 置 `unmatched` 并复用 `ChatService.closeConversationForMatch` 触发 `conversation.closed` 通知对方 |
| **D-21-E** | 🟡 MEDIUM(死代码矛盾) | `closeAccount` 在 blockers 处硬抛异常,使 `cancelXxx`/`invalidateLocalEligibility` 永远不可达;且给用户注销设障碍 | 改为"信息性日志 + 继续清理"(尊重「注销不设置障碍」+ 计划 520「关闭未来服务」);`checkCanWithdraw` 仍作为 GET 端点向用户/运营展示待处理业务 |
| **D-21-F** | 🟡 MEDIUM(假绿测试) | 7×`assertTrue(true)` 零断言 | 删除,替换为 11 用例真实 DB 集成测试 + 3 用例登录守卫 |
### 设计修正说明(D-21-E)
原 `closeAccount` 在 `checkCanWithdraw` 失败即 `throw`。这导致"关闭未来服务"的清理逻辑**在结构上不可达**(典型的"写了清理代码却永远跑不到"死代码),并且与类注释「注销必须在线便捷、不设置障碍」冲突。本轮改为:先调 `checkCanWithdraw` 仅作**信息性日志**,随后**继续**执行取消/退出/失效/关闭 Match/脱敏。这是一次有意为之的行为修正,理由:
1. AGENTS「注销必须在线便捷、不设置障碍」——不能要求用户先手动取消所有业务才能注销;
2. 计划 520「注销事务关闭未来服务」——清理必须由注销流程自己完成;
3. 让原已写好却不可达的 `cancelXxx` 逻辑真正生效。
> ⚠️ 此修正改变了"注销在有进行中业务时是否阻断"的行为。**原有行为是硬阻断**,现已改为"提示 + 自动清理"。若产品方认为某些业务(如 `in_progress` 咨询)应**先经用户确认**再注销,属于产品决策,需另行评估——本轮按"不设置障碍 + 自动关闭"实现并出报告。
---
## 三、跨模块验证(既有,确认无误)
- **推荐引擎** `RecommendationEngineService:329`:非 `accountStatus = NORMAL` 用户(含 `CLOSED`)被排除 → 注销后不再进入推荐池。✅(既有)
- **匹配资格** `MatchEligibilityService:149`:目标用户非 `NORMAL` 则不可匹配 → 注销后不可被匹配。✅(既有)
- **Capability** `CapabilityService:46,67`:`NORMAL` 才可展示能力入口。✅(既有)
这三处与 D-21-A/D-21-D 共同构成 Gate「注销后不能登录/推荐/聊天」的完整防线。
---
## 四、验证证据
| 项 | 结果 |
|---|---|
| 后端全量回归 | **650 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(起点 648,净 +2) |
| 新增守卫 | `AccountLifecycleServiceTest`(11 用例:checkCanWithdraw 各拦截项 + closeAccount 全链路 + 关闭 Match)+ `AuthServiceAccountClosedGuardTest`(3 用例:CLOSED 拒登 / NORMAL 不误伤 / BANNED 仍拒) |
| 负向验证(NEG-VAL) | 3 处注入 → **3 用例精准变红、零无关失败**:① 移除 `checkAccountStatus` 的 CLOSED 分支 → `closedAccountCannotLogin` 变红(经 `ReflectionTestUtils` 直接调私有方法,**与 Redis 解耦**,避免 CI 有 Redis 时守卫失效);② 移除 Match 置 `unmatched` → `closeAccountClosesActiveMatch` 变红;③ 移除身份证脱敏 → `closeAccountProceedsAndCleansUpActiveServices` 变红。还原后 `md5sum` 逐字节一致、残留 0 |
| Git | commit + tag `phase21-recovery-2026-09-18` + bundle(回读通过),工作区干净 |
---
## 五、遗留 / 待确认(未静默扩权)
1. **红娘服务进行中拦截** —— `checkCanWithdraw` 的 `activeMatchmakerServices` 仍硬编码 `0`。根因:当前数据模型**没有 `matchmaker_services` 表**(全仓搜索为空),该拦截项在设计上无法落地。本轮**未**凭空建表(属扩权),标注为遗留 TODO,建议产品确认红娘服务的存储模型后再补。
2. **数据保留"配置 + 人工审核"删除流程** —— 计划 521 要求"数据保留期限未确认的敏感材料采用配置和人工审核,不擅自永久保存或删除"。本轮仅做到"脱敏保留"(PII 已脱敏),**可配置保留期限 + 人工审核删除流程**属产品决策,未实现,建议单列任务。
3. **隐私设置接口** —— `getPrivacySettings`/`updatePrivacySettings` 仍是 TODO 桩(计划未将其列入 Phase 21 强制项),返回假数据/不更新,属遗留。
---
## 六、改动清单
**核心修复(生产)**
- `auth/service/AuthService.java` —— `checkAccountStatus` 增加 `CLOSED` 拒绝。
- `account/service/AccountLifecycleService.java` —— 注入 `MatchMapper`/`ChatService`/`UserIdentityMapper`/`CounselingFollowupMapper`;实现 `notifyActiveMatches`/`cancelPendingActivityRegistrations`;接线并扩展 `anonymizeServiceHistory`;扩展 `sanitizeUserData` 身份证脱敏;`closeAccount` 取消 blockers 硬阻断改为信息性日志+继续清理。
**测试(保真度)**
- `account/AccountLifecycleServiceTest.java` —— 由 7×`assertTrue(true)` 重写为 11 用例真实集成测试。
- `auth/AuthServiceAccountClosedGuardTest.java` —— 新增 3 用例(Redis 解耦)。
**文档**
- `docs/Phase-21-V11-Recovery-Completion-Report-2026-09-18.md`(本文件)
- `docs/STATUS.md` / `docs/JIRA_BACKLOG.md` —— 同步复验结论与真实缺陷。