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

103 lines
8.1 KiB
Markdown
Raw Permalink 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 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)未自动进入,等待用户确认后启动。** 是否继续?