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

8.1 KiB
Raw Permalink Blame History

Phase 18 — 申诉 KEEP-VERIFY 复验完成报告

日期:2026-09-17 范围:申诉(appeal 模块、Miniapp appeal 页面、后台 AppealCenter) 纪律:KEEP-VERIFY。历史 ✅ 不可信;测试覆盖 ≠ 运行时路径覆盖;逐条核对运行时路径与生产约束;发现真实缺陷即修;补测试保真度;守卫必须解析真实 DDL;离线容器真实编译+执行;每阶段出报告后停止等确认。


一、结论

Gate 达成,复验关闭。

Gate 结果
申诉通过只解除目标处置 ✅ approveAppeal 只把 relatedActionId 指向的处置置 resolved;用户其他仍有效的处置保持 active(负向验证已证守卫有牙)
拒绝后原处置保持有效 ✅ rejectAppeal 不变更 risk_action,原处置维持 active,账号仍受限
首发由 Feature Flag 控制 ✅ features.appeal-enabled(dev/docker 均 false);关闭时 submitAppeal 抛"申诉功能暂未开放"

本轮修复真实缺陷 8 处(含 1 处必崩级测试盲区、2 处「只在 PG 上炸」的生产故障),新增集成测试 15 例,删除假绿测试 9 例。

  • 全量回归:494 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(约 4.4 分)
  • 基线 488 → 494,净 +6(−9 假绿 AppealServiceTest,+15 AppealPhase18IntegrationTest)
  • 负向验证:临时让 approveAppeal 误伤用户其他有效处置 → approveResolvesOnlyTargetActionAndKeepsOthersActive 精准变红,已还原
  • admin-web vue-tsc --noEmit EXITCODE=0

二、本轮最关键发现(必记)

F1:申诉整条真实 DB 路径「零覆盖」盲区(必崩级)

appeals 表只列在 EntityMigrationParityTest.TEST_SCHEMA_OPTIONAL_TABLES 白名单里,而 H2 schema-h2.sql 根本没有该表 → submitAppeal / processAppeal / approve / reject / escalate 整条真实 DB 路径零覆盖。 更严重的是 AppealServiceTest:9 个用例中 6 个是 assertTrue(true) 假绿,而它们"覆盖"的恰好是 Gate 四条不变量(仅 ban/suspend 可申诉、幂等、通过解除、拒绝维持);另有 1 个用 @Mock 断言 Mockito 默认值(testFeatureFlagDefaultDisabled 测的是 mock 不是真实默认值)。 这是「白名单 = 无人测 + 占位假绿」模式的第八次复现。

F2:escalate 向 operator 发用户通知 → 撞 FK(只在 PG 上炸)

escalateAppeal 调用 notificationService.send(operatorId, ...),但 operators 与 users 是两张独立表(V001),notifications.user_id 有 FOREIGN KEY ... REFERENCES users(id)(V004,从未移除)→ 在 PG 上必然 FK 违约,@Transactional 整体回滚 → 升级人工 100% 失败(500)。测试库 notifications 无 FK,故零覆盖。

F3:appealType 未校验白名单(只在 PG 上炸)

submitAppeal 直接透传 dto.getAppealType(),生产 chk_appeal_type CHECK (appeal_type IN ('ban_appeal','suspend_appeal')) 会拒绝非法值 → 500。测试库无 appeals 表故零覆盖。


三、真实缺陷清单(对应 Gate)

编号 类别 缺陷 修复
D1 测试盲区/必崩级 appeals 表缺失 + AppealServiceTest 6 处 assertTrue(true) 假绿 → 申诉真实 DB 路径零覆盖 补 appeals 表(对齐 V013,去外键留两 CHECK);移出白名单入 REQUIRED_IN_TEST_SCHEMA;删除假绿测试,新建 AppealPhase18IntegrationTest(15 例)
D2 生产故障(PG) escalateAppeal 向 operatorId 发用户通知 → 撞 notifications.user_id → users(id) FK → 升级事务回滚 移除非法通知,升级后进入后台待办队列由风控员决策
D3 生产故障(PG) appealType 未校验 chk_appeal_type 白名单;且未与处置类型一致性校验 校验白名单 + 必须与处置类型匹配;缺省按处置类型推导
D4 功能死胡同 escalated 状态无法再处理(processAppeal 仅接受 pending/reviewing)→ 升级后申诉永久滞留;前端 AppealCenterView 处理按钮也仅 pending 后端原子认领放行 escalated;getPendingAppeals 返回 pending+escalated;前端放开处理入口
D5 并发 processAppeal 先读后写无原子守卫 → 两运营并发处理同一申诉可相互覆盖(approve/reject 竞态) 新增 markReviewingIfProcessable 原子认领(WHERE status IN ('pending','escalated')),仅赢家继续
D6 数据一致性 AppealMapper.findByUserId / findPending / findByStatusPaged 未过滤 deleted_at(countByStatus 有) 三处补 deleted_at IS NULL
D7 可维护性 approveAppeal 硬编码 "resolved" 改用 Constants.RISK_ACTION_STATUS_RESOLVED
D8 前后端契约 miniapp 传 evidenceFileIds,后端只读 evidence → 证据字段名不匹配、永为 null DTO 增 evidenceFileIds,服务层序列化映射进 evidence

说明:RISK_ACTION_STATUS_REVOKED("revoked")在 Constants 中定义但从无使用点,且不在生产 chk_risk_action_status(active/resolved/expired)白名单内;approveAppeal 实际使用 resolved(正确)。JIRA 旧注"申诉通过 → status→revoked"与实现不符,已在文档中校正为 resolved。


四、负向验证(守卫有牙)

  • 临时在 approveAppeal 中追加"把用户所有 active 处置一并置 resolved"(模拟"绕过其他处置")→ 定向运行 AppealPhase18IntegrationTest → 恰好 1 条用例精准变红:approveResolvesOnlyTargetActionAndKeepsOthersActive 报 expected: <active> but was: <resolved>("其他仍有效的处置不得被解除(Gate)")。已还原,grep NEG-VAL = 0。

五、证据(实测,非声明)

  • 全量回归:494 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(离线容器 danyang-match-backend-test mvn -B -o test,约 4.4 分)。
  • 定向编译验证:mvn -B -o test-compile 通过(284 主源 + 78 测试源)。
  • 前端类型检查:admin-web vue-tsc --noEmit EXITCODE=0。
  • 新增测试:backend/src/test/java/com/danyang/match/platform/appeal/AppealPhase18IntegrationTest.java(15 例:真实落库/审计、仅 ban/suspend 可申诉、幂等、Gate A 通过只解除目标处置、Gate B 拒绝维持原处置、原子认领唯一赢家、二次处理被拒、escalate 后可再决策、appealType 白名单/一致性/缺省推导、证据字段映射、软删过滤、Feature Flag 关闭拦截)。
  • 删除:backend/src/test/java/com/danyang/match/platform/appeal/AppealServiceTest.java(6 假绿 + mock 默认值断言)。

六、改动文件清单

主代码(src/main)

  • appeal/service/AppealService.java — 白名单校验、原子认领、escalate 去非法通知、常量替换、证据映射、软删过滤、escalated 可再决策。
  • appeal/mapper/AppealMapper.java — 三处补 deleted_at IS NULL;新增 findPendingOrEscalated 与 markReviewingIfProcessable。
  • appeal/dto/AppealSubmitDTO.java — 新增 evidenceFileIds(对齐 miniapp 契约)。
  • shared/constant/Constants.java — 新增 APPEAL_TYPE_BAN / APPEAL_TYPE_SUSPEND。

测试(src/test)

  • appeal/AppealPhase18IntegrationTest.java — 新增 15 例。
  • appeal/AppealServiceTest.java — 删除(假绿)。
  • deployment/EntityMigrationParityTest.java — appeals 移出白名单、入 REQUIRED_IN_TEST_SCHEMA(Phase 18 注释)。
  • resources/schema-h2.sql — 补 appeals 表。

前端

  • admin-web/src/modules/appeal/AppealCenterView.vue — escalated 申诉放开处理入口(列表按钮 + 详情决策表单)。

文档

  • docs/STATUS.md、docs/JIRA_BACKLOG.md、docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md 的 Phase 18 段。
  • docs/Phase-18-V11-Recovery-Completion-Report-2026-09-17.md(本报告)。
  • 技能 danyang-platform-verification 同步更新。

七、下一步

Phase 19(统一运营后台 + 用户端公益服务页面 — ADD/REFACTOR)未自动进入,等待用户确认后启动。 是否继续?