16 KiB
线索模块缺陷报告(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 |
建议整改顺序
- 先修 P0-1、P0-2、P0-3、P0-4、P0-5(规则/上限/删除守卫,防数据错乱与越权)
- 再修 P0-6、P0-7(公海展示 + 新增归属)
- P1 批次:草稿清理(P1-1) → 删除权限(P1-2) → 反馈校验(P1-4) → 联动刷新(P1-5/P1-6) → 历史(P1-3) → 统计聚合(P1-7) → 定时任务顺序(P1-8)
- P2 收尾:后端兜底校验(幂等、数值、唯一、角色隔离、状态白名单)
注:本报告仅输出需整改项,不含代码改动。
tmp/test-lead.html可对各接口做经验性验证(重点验 P0)。