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.

238 lines
16 KiB

3 weeks ago
# 线索模块缺陷报告(A2 线索 / A7-3-1 线索规则·公海池)
> 范围:`crm-lead`(线索业务)+ `crm-rule`(线索规则/公海池配置)
> 依据:原型(蓝湖 A2-1-1~A2-1-5、A7-3-1)+ 线索业务 PRD v1.2 + 现有代码
> 说明:**仅列出需要整改项**,已实现且符合预期的部分不重复罗列。
> 状态常量:UNDISTRIBUTED=1 待分发 / PENDING=2 待领取 / CLAIMED=3 已领取 / FOLLOWING=4 跟进中 / CONVERTED=5 已转商机 / EXPIRED=6 过期失效 / VOID=7 作废
---
## 严重程度约定
| 级别 | 含义 |
|------|------|
| **P0** | 阻断性:违背核心业务约束,会造成数据错乱 / 越权 / 规则失效,必须先修 |
| **P1** | 严重:功能不完整或与原型/PRD 有明显出入,影响验收 |
| **P2** | 一般:健壮性、性能、体验类问题 |
---
## P0 阻断性缺陷
### P0-1 删除公海池未校验"存在活跃线索",允许删除有绑定线索的池
- **位置**:`crm-rule/.../service/impl/LeadPoolServiceImpl.java` → `deletePool()` / `deleteBatch()`
- **原型依据**(A7-3-1 线索规则):
> "删除:仅超级管理员可见;单条删除前弹出二次确认;**已存在线索绑定的公海池禁止删除**。"
- **代码现状**:`deletePool()` 直接软删除,未做任何线索占用检查。`RuleConstants.CODE_POOL_HAS_ACTIVE_LEAD = 64005` 已定义但**从未被引用**。
- **影响**:删除后,该池下 UNDISTRIBUTED/PENDING/CLAIMED/FOLLOWING 线索变成孤儿(poolId 指向已删除池),公海列表、回收/失效定时任务、领取拦截全部错乱。
- **整改建议**:删除前执行
`SELECT COUNT(*) FROM lead WHERE pool_id = ? AND status IN (1,2,3,4)`
> 0 时抛 `CODE_POOL_HAS_ACTIVE_LEAD(64005)`。批量删除逐个校验或聚合校验后整体拒绝。
---
### P0-2 领取线索未校验"个人持有上限 / 个人每日领取上限"
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `claimLead()`
- **原型依据**(A7-3-1 其他设置 / A2-1-1 线索公海):
> "个人每日领取上限:限制单个销售每日从本公海池最多领取线索条数……输入 0 则代表无每日领取限制"
> "个人持有上限:单个销售名下同时持有的、未释放/未转商机的本公海线索最大总量……**达到上限后销售无法再领取该池线索**"
> "当销售达到每日领取上限、个人持有上限时,线索公海页面【领取】按钮置灰不可点击,悬浮提示已达对应上限。"
- **代码现状**:`claimLead()` 仅做状态 CAS(PENDING→CLAIMED),**无 hold_limit、无 daily_claim_limit 校验**。
- **影响**:销售可无限领取,突破公海池配置的持有/每日上限,规则形同虚设。
- **整改建议**:领取前,按目标池配置校验:
- 持有上限:`COUNT(*) FROM lead WHERE claim_user_id=? AND pool_id=? AND status IN (3,4)` ≥ `hold_limit` → 抛 65007(limit=0 跳过)
- 每日上限:统计当日该用户从该池领取历史条数 ≥ `daily_claim_limit` → 抛 65006(limit=0 跳过)
---
### P0-3 管理员分配线索给销售时,未校验被分配人的持有/每日上限
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `assignToUser()`
- **原型/PRD 依据**:分配与领取共用同一套"个人持有上限"约束(PRD §6.3);分配等价于让被分配人"持有"该线索,同样受上限约束。
- **代码现状**:`assignToUser()` 无任何上限校验。
- **影响**:管理员分配可绕过上限,导致某销售名下线索超过持有上限,破坏规则一致性。
- **整改建议**:以**被分配人(assignee)**为主体,用**目标池**的 `hold_limit`/`daily_claim_limit` 做与 P0-2 相同的两项校验,超限抛 65007/65006。
---
### P0-4 新增线索并勾选"直接领取"时,绕过领取规则与上限校验
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `createLead()`(`claimOnCreate=true` 分支)
- **原型依据**(A2-1-2-2 新增线索):
> "是否领取:复选框,勾选则新增完成后自动领取该线索"
- **代码现状**:`claimOnCreate=true` 时直接把线索置为 CLAIMED 并赋 claim_user,**未走 claimLead 的规则/上限校验链路**。
- **影响**:销售通过"新增即领取"可绕过 claimRule(管理员可分配模式下本不应能自领)与持有/每日上限,形成后门。
- **整改建议**:`claimOnCreate=true` 时复用 P0-2 的校验(claimRule + hold_limit + daily_claim_limit);不满足则创建为公海待领取状态或直接拒绝(按产品定稿)。
---
### P0-5 领取规则"管理员可分配"未拦截销售自主领取
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `claimLead()`
- **原型依据**(A7-3-1):
> "选项2:管理员可分配 —— 销售仅能查看公海线索,**无自主领取按钮**,所有线索只能由管理员统一分配下发。"
- **代码现状**:`claimLead()` 未读取所属池的 `claim_rule`,任何销售都能领取任意池线索。
- **影响**:设置为"仅管理员可分配"的公海池,销售仍可自领,权限模型被击穿。
- **整改建议**:领取前读取池 `claim_rule`;若为"仅管理员可分配",销售主体领取直接抛 `CODE_CLAIM_RULE_DENIED(65005)`。(前端置灰是展示层,后端必须兜底。)
---
### P0-6 线索公海列表硬编码只展示"待领取",无法防撞单混合展示
- **位置**:`crm-lead/.../query/impl/LeadViewQueryImpl.java` → `buildViewWrapper()`(PUBLIC_POOL 分支)
- **原型依据**(A2-1-1 列表视图):领取人列展示对应销售姓名,线索状态含"待领取/已领取/跟进中",且规则强调"一条线索仅可被 1 人领取""当前线索已被其他销售抢先认领……可关注该线索,待对方释放后再次认领" —— 说明公海需**混合展示已被领取的线索**用于防撞单,而非只显示待领取。
- **代码现状**:`buildViewWrapper()` PUBLIC_POOL 硬编码 `status = PENDING`
- **影响**:公海只能看到待领取线索,销售无法看到"哪些已被别人领走",无法防撞单;与原型的领取人/状态列展示不符。
- **整改建议**:PUBLIC_POOL 视图改为可配置状态集合(至少 `{PENDING, CLAIMED, FOLLOWING}`),并保证"领取人"字段随状态回填。
---
### P0-7 新增线索缺少角色约束(poolId 为空时应限管理员)
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `createLead()`
- **原型依据**(A2-1-4 线索管理 / A2-1-2-2):销售新增线索**默认归属当前销售所属公海池,不可编辑**(`poolId` 由后端根据当前用户回填);管理员新增可切换任意公海池。销售不应能传入任意 poolId 或空 poolId 创建"未分发"线索。
- **代码现状**:`createLead()` 未做角色判定;当 `poolId=null` 时的归属/权限处理缺失。
- **影响**:普通销售可能创建未分发线索或指定非自身池,越权。
- **整改建议**:非管理员且 `poolId=null` 时拒绝;销售创建强制回填其所属池,忽略前端传入的 poolId。
---
*(P1 / P2 见后续追加)*
---
## P1 严重缺陷
### P1-1 反馈提交后未清理草稿(DRAFT)行
- **位置**:`crm-lead/.../state/impl/LeadTransitionImpl.java` → `applyFeedback()`
- **原型依据**(A2-1-2-1 我的线索详情):反馈分"草稿/提交",历史记录 Tab 只应展示已提交反馈;草稿是临时暂存,提交后不应残留。
- **代码现状**:`applyFeedback()` 插入 SUBMITTED 反馈行,但未 DELETE/UPDATE 同一 `(user_id, lead_id)` 的 DRAFT 行。
- **影响**:草稿残留,下次进入反馈弹窗回显旧草稿,或统计/历史混入草稿数据。
- **整改建议**:提交成功后删除(或状态置为已提交)该用户该线索的 DRAFT 行。
### P1-2 删除线索未真正校验创建人/角色权限
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `deleteLead()`
- **原型依据**(A2-1-4):
> "删除:仅管理员角色可见;过期失效、未分发、待领取线索可删除,已领取/已转商机线索删除增加二次强确认。"
- **代码现状**:`deleteLead()` 计算了 `isCreator` 变量,但**从未用于判定**,删除权限实际不受控。
- **影响**:非管理员/非创建人可能删除他人线索,越权。
- **整改建议**:按角色(管理员)+ 状态白名单(EXPIRED/UNDISTRIBUTED/PENDING 可删)落地校验;`isCreator` 若为业务需要则真正参与判定,否则移除死变量。
### P1-3 编辑线索未记录完整字段变更历史
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `editLead()`
- **原型依据**(A2-1-2-1 历史记录 Tab):
> "操作类型标签(领取/反馈/释放/转商机/编辑)、完整操作详情文本""所有操作永久留存至历史记录"
- **代码现状**:`editLead()` 更新字段但未记录 old→new 字段级差异到 history。
- **影响**:历史记录 Tab 的"编辑"日志缺少变更详情,无法审计"改了什么"。
- **整改建议**:编辑时对全部业务字段做 diff,将变更项(字段名/旧值/新值)写入 EDIT 类型 history。
### P1-4 提交反馈缺少入口级状态合法性校验
- **位置**:`crm-lead/.../service/impl/LeadServiceImpl.java` → `submitFeedback()`
- **原型依据**(A2-1-2-1):
> "反馈:仅【未过期失效】线索可见……过期失效线索隐藏此按钮";反馈情况仅"有效/无效/未反馈"。
- **代码现状**:`submitFeedback()` 未在入口校验 `feedbackStatus ∈ {有效,无效}`,也未拦截 EXPIRED/VOID/CONVERTED 线索的反馈。
- **影响**:非法反馈状态值或对失效/作废/已转商机线索反馈,产生脏数据。
- **整改建议**:入口校验反馈状态枚举合法;线索状态不在可反馈集合(CLAIMED/FOLLOWING)时拒绝。
### P1-5 公海池归属(部门/省份)变更后,存量线索归属未刷新
- **位置**:`crm-rule` 池编辑 → `PoolChangedEvent` 监听器(`crm-lead` 侧)
- **原型依据**(A7-3-1):
> "联动更新:所有线索模块公海池下拉、领取按钮、上限拦截逻辑同步刷新"
- **代码现状**:需确认池 dept/省份变更时,UNDISTRIBUTED/PENDING 线索的 `dept_id` 是否同步刷新。
- **影响**:池改归属后,存量线索仍指向旧部门,数据可见性/分配错乱。
- **整改建议**:`PoolChangedEvent` 监听器对该池下未领取线索批量刷新 `dept_id` 等归属字段。
### P1-6 回收天数 N / 失效天数 M 修改后,存量 deadline 未按新规则刷新
- **位置**:`PoolChangedEvent` 监听器 / 定时任务
- **原型依据**(A7-3-1):
> "规则联动生效:修改领取上限、回收天数、失效天数后,**已领取/存量线索次日起按照新规则执行**,存量已超时线索当日不批量回溯处理。"
- **代码现状**:需确认改 N/M 后是否重算 `recycle_deadline`/`expire_deadline`。
- **影响**:改配置后存量线索仍按旧 deadline 回收/失效,与"次日起按新规则"不符。
- **整改建议**:N/M 变更时批量重算存量线索 deadline(以"次日起生效、不回溯已超时"为准)。
### P1-7 视图统计走内存循环,未用 SQL 聚合
- **位置**:`crm-lead/.../query/impl/LeadViewQueryImpl.java` → `countStats()`
- **原型依据**(A2-1-1/A2-1-3):顶部统计卡片"线索总量/待领取/今日新增""有效/无效/未反馈/已转商机"需随筛选实时统计。
- **代码现状**:`countStats()` 全量 `selectList` 后在 Java 内存里循环累加。
- **影响**:大数据量下性能差、内存压力大,统计随数据增长退化。
- **整改建议**:改为 SQL `GROUP BY status`(及反馈情况维度)聚合,带上当前筛选条件。
### P1-8 定时任务需保证"先失效后回收"的执行顺序
- **位置**:`crm-lead` 定时任务(回收/失效)
- **PRD 依据**(v1.1 grill:定时任务顺序):失效判定(M)与回收(N)若为两个独立 `@Scheduled`,并发/顺序不确定会导致同一线索状态判定竞争。
- **整改建议**:在同一方法内串行执行(先失效判定、再回收),或用统一调度顺序,避免两个独立任务竞态。
---
## P2 一般缺陷 / 体验与健壮性
### P2-1 领取并发防抖/幂等(后端兜底)
- **原型依据**(A2-1-1):"领取按钮需支持防抖(500ms)+请求中置灰+成功后禁用3秒""多人同时点【领取】时,随机选择成功人员"。
- **建议**:前端防抖是体验层;后端必须以状态 CAS(PENDING→CLAIMED,`WHERE status=2`)保证幂等,抢占失败返回明确"已被他人认领"错误码,供前端 toast。
### P2-2 数值输入校验(每日上限/持有上限/N/M)
- **原型依据**(A7-3-1):"仅允许≥0 整数……N、M 仅支持≥1 正整数;非法输入拦截提交"。
- **建议**:`LeadPoolDTO` 保存接口后端校验:daily/hold ≥0 整数(0=无限制);N、M ≥1 整数。前端校验不可替代后端。
### P2-3 公海池名称全局唯一
- **原型依据**(A7-3-1):"占位提示【输入名称】,全局唯一不可重复"。
- **建议**:`saveOrUpdate` 时校验名称唯一,重复抛业务错误码。
### P2-4 部门负责人编辑时归属字段应只读(后端拒绝越权改归属)
- **原型依据**(A7-3-1):"部门负责人仅可修改本部门公海池人员配置,归属类下拉(团队/部门/省份)置灰不可编辑""无法修改归属团队/部门/省份"。
- **建议**:后端在 `saveOrUpdate` 中,若操作者为部门负责人,忽略/拒绝对 team/dept/province 的变更,仅接受人员与规则字段。
### P2-5 批量操作按钮的角色隔离(后端校验)
- **原型依据**(A7-3-1/A2-1-4):新增/导入/导出/批量删除仅超管;部门负责人仅查询+单条编辑。
- **建议**:`delete-batch`、批量分配等接口后端校验角色,非超管拒绝,防止绕过前端隐藏直接调接口。
### P2-6 反馈情况三态与"最后反馈时间"回填
- **原型依据**(A2-1-2):列表含"反馈情况(有效/无效/未反馈)""反馈内容""最后反馈时间"。
- **建议**:确认这些派生字段在列表查询中正确回填(最近一条已提交反馈),避免草稿或空值污染。
### P2-7 过期失效/作废/已转商机线索的操作按钮收敛
- **原型依据**(A2-1-2-1):"已转商机隐藏释放;过期失效隐藏反馈/释放,仅编辑/取消关注"。
- **建议**:后端对 release/feedback/convert 接口按状态白名单兜底拒绝,不依赖前端隐藏。
---
## 原型 ↔ 代码 差异速查表
| 原型要求(出处) | 代码现状 | 缺陷编号 |
|---|---|---|
| 已绑定线索的公海池禁止删除(A7-3-1) | 无守卫,64005 未用 | P0-1 |
| 个人持有/每日领取上限拦截(A7-3-1/A2-1-1) | claim 无校验 | P0-2 |
| 分配同受持有上限约束(PRD §6.3) | assign 无校验 | P0-3 |
| 新增即领取(A2-1-2-2) | 绕过规则/上限 | P0-4 |
| "仅管理员可分配"禁止销售自领(A7-3-1) | 未读 claim_rule | P0-5 |
| 公海混合展示防撞单(A2-1-1) | 硬编码 status=PENDING | P0-6 |
| 销售新增归属本人池不可编辑(A2-1-2-2) | 无角色约束 | P0-7 |
| 反馈草稿/提交分离(A2-1-2-1) | 提交后未清草稿 | P1-1 |
| 删除仅管理员+状态白名单(A2-1-4) | isCreator 死变量 | P1-2 |
| 编辑写完整变更历史(A2-1-2-1) | 无字段级 diff | P1-3 |
| 反馈仅未失效线索+状态枚举(A2-1-2-1) | 入口无校验 | P1-4 |
| 池归属变更联动刷新(A7-3-1) | 存量未刷新 | P1-5 |
| 改 N/M 次日起生效(A7-3-1) | 存量 deadline 未重算 | P1-6 |
| 统计卡片实时聚合(A2-1-1/3) | 内存循环 | P1-7 |
---
## 建议整改顺序
1. **先修 P0-1、P0-2、P0-3、P0-4、P0-5**(规则/上限/删除守卫,防数据错乱与越权)
2. **再修 P0-6、P0-7**(公海展示 + 新增归属)
3. **P1 批次**:草稿清理(P1-1) → 删除权限(P1-2) → 反馈校验(P1-4) → 联动刷新(P1-5/P1-6) → 历史(P1-3) → 统计聚合(P1-7) → 定时任务顺序(P1-8)
4. **P2 收尾**:后端兜底校验(幂等、数值、唯一、角色隔离、状态白名单)
> 注:本报告仅输出需整改项,不含代码改动。`tmp/test-lead.html` 可对各接口做经验性验证(重点验 P0)。