You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

83 lines
5.5 KiB

3 weeks ago
# 10 修复:P2 健壮性批次(P2-1 ~ P2-7)
Type: task
3 weeks ago
Status: resolved
3 weeks ago
Blocked by: 04
## Question
按 ticket 04 的裁决修复 P2 确认项(判误报/已实现的跳过,仅在清单记录):
- **P2-1 领取并发**:复核 CAS 抢占失败返回明确"已被他人认领"错误码(前端 toast 依据);缺则补。
- **P2-2 数值校验**:按 04 以原型原文裁决的语义对齐 `savePool` 校验(daily ≥0 整数、0=无限;hold 按 PRD/原型结论);`LeadPoolServiceImplTest` 既有断言与新语义冲突时以裁决为准调整。
- **P2-3 池名全局唯一**:`savePool()` 校验唯一(注意软删行不计入/规避重复)。
- **P2-4 部门负责人归属只读**:按 04 结论(含用户拍板的实现方式)落地——操作者为部门负责人时忽略/拒绝对 team/dept/province 归属字段的变更。
- **P2-5 批量角色隔离**:按 04 查明的既有权限实践给 `delete-batch`/批量分配等端点补权限点校验(纯 hasAuthority + 种子机制,不发明新机制;若需新增权限点种子,走 PermissionInitializer 既有 seam)。
- **P2-6 反馈派生字段**:按 04 结论修正列表回填(最新 SUBMITTED 反馈,不被 DRAFT 污染)。
- **P2-7 状态白名单兜底**:按 04 结论复核/补齐 release/feedback/convert 的后端状态白名单(若 guard 已兜底则只补单测)。
**验收**:每项针对性单测 + `mvn test`(crm-rule/crm-lead)全绿 + BOM 扫描。
3 weeks ago
## Answer
### 判定汇总
| 编号 | 裁决 | 处理 |
|---|---|---|
| P2-1 | 误报 | CAS 幂等已落地(`eq status=PENDING` + checkCas 65010),无需修复 |
| P2-2 | 误报(hold=0 留白已解决) | validatePool 四项校验全齐(ticket 06 已修 holdLimit≥0);本票修正 DTO/实体注释"≥0,0=不可领取" |
| P2-3 | 误报 | 池名唯一已校验 + @TableLogic 软删行不计入 + deleteKey 复用键,无需修复 |
| P2-4 | **实现遗漏** | **已修复**——savePool 增加角色拦截守卫 |
| P2-5 | 实现遗漏*(用户拍板 B) | **按用户决策关闭**——维持 fail-open,不种子化 button 权限点(记最终报告风险项) |
| P2-6 | 误报 | 反馈派生字段=主表快照列,草稿明确不刷快照,无污染路径,无需修复 |
| P2-7 | 误报 | guard 声明式守卫 + editLead 终态锁已完备,无需修复 |
### P2-4 修复详情(savePool 角色拦截)
**文件**:`crm-rule/src/main/java/com/crm/rule/service/impl/LeadPoolServiceImpl.java` — `savePool()` 方法
**变更**:
1. 方法开头获取 `isAdmin = SecurityUtils.hasRole(AuthConstants.ROLE_CODE_ADMIN)` + `exist` pool(编辑路径)
2. 非管理员编辑时(`!isCreate && !isAdmin`):
- `deptId` 变更 → 抛 `CODE_POOL_INVALID`("部门负责人不可修改归属部门,仅超级管理员可变更")
- 地理范围变更(省份/城市 ID 列表集合比较,忽略顺序) → 抛 `CODE_POOL_INVALID`("部门负责人不可修改负责区域,仅超级管理员可变更")
3. 管理员编辑时:守卫跳过,可自由修改所有字段
4. 新建时:守卫跳过(`isCreate=true`)
**设计决策**:
-`validatePool` 前获取 `exist`,避免非管理员发送不同 `deptId` 时唯一性校验误拦
- 地理范围用 `Set<Long>` 集合比较(`null` 归一化为空集),忽略列表顺序差异
- 角色判定用 `SecurityUtils.hasRole`(service 层业务规则,非 controller 层 @PreAuthorize
### P2-2 注释修正
- `LeadPoolDTO.java`:`@Schema(description = "个人持有上限(≥0,0=不可领取)")`
- `LeadPool.java`:`@Comment("个人持有上限(≥0,0=不可领取)")`
- `validatePool``holdLimit≥0` 校验已在 ticket 06 修正,此处仅同步注释
### P2-5 用户决策
用户拍板 **B:维持 fail-open**——不推翻"不种子化 button 权限点"决策。P2-5 按既有决策关闭,记入最终报告风险项。P0-7/P1-2/P2-4 的 service 内角色/管理员判定仍按 PRD 定稿做(属业务规则非 button 权限点,走 SecurityUtils + 角色查询,不走 hasAuthority + 种子)。
### 单测
**新增 4 个 P2-4 测试**(`LeadPoolServiceImplTest.java`):
| 测试 | 场景 | 验证 |
|---|---|---|
| `savePool_nonAdminChangeDept_rejected` | 非管理员编辑改 deptId | 抛 CODE_POOL_INVALID,不执行 updateById |
| `savePool_nonAdminChangeGeography_rejected` | 非管理员编辑改 geography | 抛 CODE_POOL_INVALID,不执行 updateById |
| `savePool_nonAdminNoOwnershipChange_passes` | 非管理员编辑不改归属字段 | 放行,执行 updateById + publishEvent |
| `savePool_adminChangeDept_passes` | 管理员编辑改 deptId | 放行(守卫跳过),执行 updateById + publishEvent |
**适配 1 个既有测试**:`savePool_update_publishesEvent` — 编辑路径现在进入守卫,用 `mockStatic(SecurityUtils.class)``hasRole` 返回 `true`(管理员路径),保持原有"编辑成功 + 发布事件"验证意图不变。
**mockStatic 模式**:try-with-resources 局部 scope,与 `@Mock` 字段不冲突,`MockitoExtension` strict stubbing 无误报。
### 验证
- **BOM 扫描**:4 个修改文件全绿(LeadPoolServiceImpl.java / LeadPoolDTO.java / LeadPool.java / LeadPoolServiceImplTest.java)
- **mvn test**:`mvn test -pl crm-rule,crm-lead -am` → BUILD SUCCESS
- crm-rule:30 tests(含 LeadPoolServiceImplTest 22 tests = 18 原有 + 4 新增)
- crm-lead:97 tests(含 ticket 09 新增的 PoolChangedEventListenerTest / LeadMaintenanceJobTest / 更新的 LeadViewQueryImplTest)