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.

99 lines
13 KiB

3 weeks ago
# 确认清单 — P1(缺陷报告 P1-1 ~ P1-8 三方判定)
> 方法:原型条款([research/原型依据基准.md](research/原型依据基准.md))× PRD v1.2 × 代码现状(2026-08-19 逐行复核)+ ADR-0019/0023/0021/0022 定稿口径。
> 语义优先级:PRD > 原型 > 缺陷报告;ADR 为架构定稿,与 PRD 同级引用。
> 判定口径:**真实缺陷** = 已有代码路径在特定场景产生错误结果(越权/脏数据/竞态);**实现遗漏** = 定稿要求的逻辑从未实现;**误报** = 代码已满足 PRD/原型。
## 一、汇总
| 编号 | 裁决 | 一句话结论 | 修复票 |
|---|---|---|---|
| P1-1 | **实现遗漏** | `applyFeedback` 提交后未清除同 (user_id, lead_id) 的 DRAFT 行 | 08 |
| P1-2 | **真实缺陷** | `deleteLead` 权限不受控:isCreator 死变量 + "DataScope 兜底"假设不成立,同部门销售可互删 | 08 |
| P1-3 | **实现遗漏** | EDIT 历史只记 leadName 单值,无字段级 old→new diff | 08 |
| P1-4 | **误报** | 守卫已下沉状态机(FeedbackCmd 白名单 + requiresOwner + 枚举校验),报告还误把 VOID 当禁区 | —(票 08 仅复核单测覆盖) |
| P1-5 | **误报** | `PoolChangedEventListener` 已完整实现 §4.1.1 联动(未分发/待领取 dept_id 跟池走) | —(无需修复) |
| P1-6 | **实现遗漏** | N/M 变更后存量 deadline 重算完全缺失(M 有注释自认"暂不刷新",N 连注释都无) | 09 |
| P1-7 | **真实缺陷** | `countStats` selectList 内存循环,违反 ADR-0023 D3"按 status 分组计数"定稿 | 09 |
| P1-8 | **真实缺陷** | 失效/回收是两个独立 @Scheduled(02:00/02:05)软间隔,无 ADR-0019 要求的顺序硬保证,存在竞态窗口 | 09 |
**裁决分布:实现遗漏 3 / 真实缺陷 3 / 误报 2 / 需求变更 0。**
缺陷报告 P1 段可信度:8 项中 6 项属实、2 项误报(P1-4 部分失实+建议与 PRD 冲突、P1-5 报告自身"需确认"的疑虑经查已实现)。
## 二、分项证据链
### P1-1 反馈提交后未清理草稿(DRAFT)行 — 实现遗漏
- **原型**:无草稿条款(基准 §P1-1:报告引文"A2-1-2-1 反馈分草稿/提交"在原型中不存在,实为 PRD §6.4 内容——引文失实但结论方向由 PRD 支撑)。
- **PRD**:§6.4【保存】副作用第 4 条:"**清除同 (user_id, lead_id) 的草稿行(转为 SUBMITTED)**"。
- **代码**:[LeadTransitionImpl.applyFeedback()](../../crm-lead/src/main/java/com/crm/lead/state/impl/LeadTransitionImpl.java#L226-L237) L226-237 仅 `leadFeedbackMapper.insert(fb)` 写入新 SUBMITTED 行,**无任何 DRAFT 行的 DELETE/UPDATE**。用户"先存草稿再提交"时 DRAFT 行永久残留。
- **影响修正**:Controller 仅有 `/feedback-draft` POST(保存草稿),**无草稿查询回显接口**——残留当前无用户可见影响,属数据层脏行 + 未来回显接口的坑。
- **修复方向(票 08)**:提交事务内清除同 (user_id, lead_id) 的 DRAFT 行。PRD 措辞"转为 SUBMITTED"——按现表结构建议:有 DRAFT 则 UPDATE 该行为 SUBMITTED(内容=提交内容),无则 INSERT,保证 (user_id, lead_id, DRAFT) 唯一。
### P1-2 删除线索未真正校验创建人/角色权限 — 真实缺陷
- **原型**:A2-1-4"仅管理员角色可见;过期失效、未分发、待领取可删除,已领取/已转商机删除增加二次强确认"——**已被 PRD §3.3 推翻**(需求变更背景,修复按 PRD 不按原型)。
- **PRD**:§3.3 删除权限全局规则(2026-08-13 定稿):`删除 = (状态 ≠ 已转商机) AND (管理员 OR 创建人·强确认)`;**已转商机是唯一删除禁区**;过期失效/作废无下游依赖仍可删。§9 定稿写权由"权限点 + service"守。
- **代码**:[LeadServiceImpl.deleteLead()](../../crm-lead/src/main/java/com/crm/lead/service/impl/LeadServiceImpl.java#L191-L207)——L193-195 已拦截 CONVERTED ✅;**L197-199 计算 `isCreator` 后从未使用(死变量)**;L200-201 注释声称"管理员判定由 DataScope 兜底(ALL_VISIBLE 能查到即可删)"——**该假设不成立**:@DataScope 是 SELECT 读过滤(四档天花板 SELF/DEPT/DEPT_AND_CHILDREN/ALL),`getById` 查得到 ≠ 是管理员——**SELF/DEPT 档的普通销售查得到本部门他人线索即可删除**,写操作无任何守卫。
- **影响**:同部门销售可互删他人线索;删除行为仅剩 CONVERTED 一道闸。
- **修复方向(票 08)**:service 内显式落地 `(状态≠CONVERTED) AND (管理员 OR 创建人)`;管理员判定机制与票 07(P0-7 角色分支)**共用同一实现**保持一致;死变量激活或移除;DataScope 兜底注释更正。
### P1-3 编辑线索未记录字段级变更历史 — 实现遗漏
- **原型**:A2-1-2-1 历史记录"操作类型标签(领取/反馈/释放/转商机/编辑)、完整操作详情文本""永久留存"→ 基准 §P1-3 ✅(引文真实)。
- **PRD**:§6.9 副作用"写 lead_history EDIT 记录,**detail 含变更字段清单(旧值→新值)**";§6.10 EDIT detail=变更字段清单(旧值→新值)。可编辑字段=基础 7(名称/电话/微信/邮箱/省/市/地址)+ 业务来源 4(渠道/品牌/需求产品/需求场景)+ 核心 2(咨询内容/是否加急)。
- **代码**:[LeadServiceImpl.editLead()](../../crm-lead/src/main/java/com/crm/lead/service/impl/LeadServiceImpl.java#L185-L186) L185-186 仅 `historyRecorder.record(id, EDIT, userId, "leadName", update.getLeadName())`——只记单字段且只有新值、无旧值→新值格式。
- **修复方向(票 08)**:editLead 对 §6.9 可编辑字段全集做 old→new diff,变更项按 `LeadHistoryRecorder` kv 机制写入(字段名/旧值/新值);无变更时是否仍写 EDIT 由票 08 定并注明依据。
### P1-4 提交反馈缺少入口级状态校验 — 误报
- **原型**:A2-1-2-1"反馈仅【未过期失效】线索可见"→ 基准 §P1-4 ✅(引文真实)。
- **PRD**:§3.3 矩阵反馈行 = 已领取 ✓(持有人) / 跟进中 ✓(持有人) / **线索作废 ✓(持有人)**(边 #16 作废恢复),其余 ✗。
- **代码**:守卫已完整下沉状态机——[TransitionCmd.FeedbackCmd](../../crm-lead/src/main/java/com/crm/lead/state/TransitionCmd.java#L86-L101) `allowedFromStatuses()={CLAIMED, FOLLOWING, VOID}` + `requiresOwner()=true`;[guard()](../../crm-lead/src/main/java/com/crm/lead/state/impl/LeadTransitionImpl.java#L311-L318) 统一执行(非法态抛 65003、非持有人抛 65004);[applyFeedback() L196](../../crm-lead/src/main/java/com/crm/lead/state/impl/LeadTransitionImpl.java#L195-L197) 拒绝 feedbackStatus ∉ {有效,无效}("反馈情况必须为有效或无效")。
- **报告失实点**:①"未校验 feedbackStatus 枚举"——L196 已校验;②"未拦截 EXPIRED/CONVERTED"——guard 白名单已拦;③建议"拦截 VOID 反馈"——**与 PRD §3.3 冲突**(作废线索被分配后新持有人反馈=有效是唯一复活路径,边 #16)。
- **处理**:无需修复。票 08 仅复核单测对 {EXPIRED, CONVERTED, PENDING, UNDISTRIBUTED} 拒反馈、非持有人拒反馈、feedbackStatus=0 拒绝三个场景的覆盖,缺则补测。
### P1-5 池归属变更后存量线索归属未刷新 — 误报
- **原型**:A7-3-1"联动更新:所有线索模块公海池下拉、领取按钮、上限拦截逻辑同步刷新"——引文真实但语义=前端刷新(基准 §P1-5 ⚠),非存量 dept_id 刷新。
- **PRD**:§4.1.1 定稿:未分发/待领取 dept_id **跟池走**(批量 UPDATE);已领取/跟进中**不动**;终态不动。§4.2 池改名时批量刷 pool_name_snapshot。
- **代码**:[PoolChangedEventListener](../../crm-lead/src/main/java/com/crm/lead/event/PoolChangedEventListener.java#L26-L48) **已实现且与 §4.1.1 完全一致**——①池名快照全量刷(L33-36);②`in(Lead::getStatus, 1, 2)` 恰为未分发/待领取刷 deptId+teamDeptId(L38-43),其余态天然不动。事件发布侧:`savePool` 编辑时发布(LeadPoolServiceImpl L128-130)✅。
- **报告失实点**:"需确认是否同步刷新"——已刷新;标题中"省份"维度系过度推度(PRD 无"线索省份跟池走"条款,线索 provinceCode 是客户属性与池无关)。
- **处理**:无需修复。票 09 按此结论**跳过 P1-5**(监听器扩展仅做 P1-6 的 deadline 重算)。
### P1-6 回收天数 N / 失效天数 M 修改后存量 deadline 未刷新 — 实现遗漏
- **原型**:A7-3-1"修改领取上限、回收天数、失效天数后,已领取/存量线索**次日起按照新规则执行**,存量已超时线索当日不批量回溯处理"→ 基准 §P1-6 ✅。
- **PRD**:§5.3 定稿刷新公式——N 改动:已领取 `recycle_deadline = claim_time + new_N`、跟进中 `= last_feedback_time + new_N`(即 lead.feedback_time 快照);M 改动:`expire_deadline = create_time + new_M WHERE status ≠ 已转商机`;**当日不批量清算已超时行**(只改截止时刻,状态转移交次日定时任务)。
- **代码现状(已有什么/缺什么——票 09 形状输入)**:
- **已有**:`PoolChangedEventListener` 骨架(池名快照 + dept_id 联动,见 P1-5)。
- **缺**:N 重算**完全无代码**(监听器无 recycle_deadline 任何逻辑);M 重算**注释自认预留**(L45-46"expireDeadline 刷新……此处暂不批量刷新(需自定义 SQL 做 DATE_ADD)")。
- **修复方向(票 09)**:监听器补两条批量重算(自定义 SQL DATE_ADD,LambdaUpdateWrapper 表达不了列间运算):N 按 §5.3 分状态两段 UPDATE;M 一段 UPDATE(status≠CONVERTED)。"不回溯清算"由"只改 deadline 不改状态"天然满足。
### P1-7 视图统计走内存循环 — 真实缺陷(性能)
- **原型**:A2-1-1/A2-1-3/A2-1-4"实时统计当前筛选条件下对应数据"——只约束实时性,不约束实现方式(基准 §P1-7)。
- **ADR**:**ADR-0023 D3 定稿**:"与 /page 吃完全相同的 viewType + 筛选 + @DataScope……**只把「取一页」换成「按 status 分组计数」**"。
- **代码**:[LeadViewQueryImpl.countStats()](../../crm-lead/src/main/java/com/crm/lead/query/impl/LeadViewQueryImpl.java#L84-L118) L87 `leadMapper.selectList(...)` **全量拉回**后 L90-116 Java 内存循环累加——方法注释自称"改分组计数"但实现名实不符,违反 ADR-0023 D3 定稿。
- **影响**:数据量增长时全结果集进内存(当前演示数据量无感,属随规模退化的性能缺陷)。
- **修复方向(票 09)**:改 SQL `GROUP BY status` 聚合(status + feedbackStatus + 今日新增三维度),复用 `buildViewWrapper` 保口径一致;注意 H2/MySQL 兼容(测试跑 H2,曾有缺列翻车教训)。
### P1-8 定时任务"先失效后回收"顺序 — 真实缺陷
- **原型**:无直接条款(权威 = PRD §6.7 + ADR-0019)。
- **PRD/ADR**:§6.7 grill #4 定稿 + **ADR-0019**(accepted):统一每日 02:00 全库扫,**失效先、回收后**;ADR-0019 Consequences 明文:"**若将来两个 Job 拆成独立调度/独立进程,必须保留「失效先完成、回收后开始」的顺序保证,否则本决策失效**"。
- **代码**:[LeadExpireJob](../../crm-lead/src/main/java/com/crm/lead/job/LeadExpireJob.java#L23-L29) `@Scheduled(cron="0 0 2 * * ?")` 与 [LeadRecycleJob](../../crm-lead/src/main/java/com/crm/lead/job/LeadRecycleJob.java#L24-L30) `@Scheduled(cron="0 5 2 * * ?")`——**两个独立 @Scheduled 靠 5 分钟 cron 软间隔**,无"失效先完成"硬保证。失效 Job 执行超 5 分钟时(大数据量)回收 Job 并发启动:双 deadline 过期线索先被回收(清 owner)再被失效(保留 owner 语义被破坏)——**owner 被误清,激活找回原持有人的后路丢失**,正是 ADR-0019 要防的场景。
- **报告核实**:报告的竞态担忧属实(其引用的是 PRD v1.1 grill,v1.2 §6.7 + ADR-0019 已定稿同义)。
- **修复方向(票 09)**:合并为单 Job——一个 `@Scheduled(cron="0 0 2 * * ?")`,方法内先 `executeExpire(now)``executeRecycle(now)` 串行(完全对齐 PRD"同一次扫描内先失效后回收"原文);两个 Job 类合一或保留类仅留单调度入口。ADR-0019 语义不变无需修订。
## 三、待用户确认
无。P1 段无"PRD 与原型均无定论"的产品分歧(P1-2 管理员判定机制为工程决策,票 07 选型后票 08 复用;P1-3 无变更是否写历史、P1-6 daily 计数数据源均为票内工程决策)。
## 四、结论
1. P1-1~P1-8 判定完毕:**实现遗漏 3(P1-1/3/6)、真实缺陷 3(P1-2/7/8)、误报 2(P1-4/5)**。
2. 两个真实缺陷均有 ADR 级依据:P1-7 违反 ADR-0023 D3 定稿、P1-8 违反 ADR-0019 Consequences 的顺序硬保证。
3. 修复映射:**票 08**(P1-1 草稿清理、P1-2 删除权限、P1-3 字段级 diff;P1-4 误报仅补单测)→ **票 09**(P1-6 N/M 重算、P1-7 SQL 聚合、P1-8 Job 合并;P1-5 误报跳过,监听器现状保留)。
4. 判定口径与 P0 清单一致:P1-2 的"原型仅管理员"被 PRD §3.3 推翻记录为需求变更背景,代码修复一律按 PRD。