# 线索模块缺陷验证与修复报告 > 范围:`tmp/缺陷报告-线索模块-A2-A7-3-1.md` 的 22 项缺陷(P0×7 / P1×8 / P2×7) > 方法:蓝湖原型(MCP 实时版 2026-08-15)× PRD v1.2 × 代码现状(2026-08-19 逐行复核) > 语义优先级:PRD > 原型 > 缺陷报告;用户决策 > PRD > 执行时间:2026-08-19 --- ## 一、缺陷确认清单(22 项全量) ### 裁决汇总 | 类别 | 数量 | 编号 | |---|---|---| | 实现遗漏 | 9 | P0-1/2/3/4/6/7、P1-1/3/6 | | 真实缺陷 | 3 | P1-2/7/8 | | 误报 | 8 | P0-5、P1-4/5、P2-1/2/3/6/7 | | 需求变更 | 0 | — | | 用户决策关闭 | 2 | P2-4(已修)、P2-5(关闭) | ### P0(7 项) | 编号 | 裁决 | 原型依据 | PRD 依据 | 代码现状(修复前) | 处置 | |---|---|---|---|---|---| | P0-1 | 实现遗漏 | A7-3-1"已存在线索绑定的池禁止删除" | §4.3"池下无非终态线索" | deletePool 无占用检查,64005 从未产出 | **票 05 已修**:跨模块 port + deletePool 守卫 | | P0-2 | 实现遗漏 | A7-3-1"达到上限禁止领取" | §6.2 前置 2/3 | claimLead 无 hold/daily 校验 | **票 06 已修**:ClaimLimitChecker | | P0-3 | 实现遗漏 | —(PRD 主导) | §6.3 分配走两上限 | assignToUser 无上限校验 | **票 06 已修**:复用 ClaimLimitChecker | | P0-4 | 实现遗漏 | A2-1-2-2"勾选则自动领取" | §6.1 副作用+§6.2 前置 | createLead claimOnCreate 绕过全部校验 | **票 06 已修**:claimOnCreate 走完整校验链 | | P0-5 | 误报 | A7-3-1"仅管理员可分配" | §6.2 前置 1 | claimLead L217 已拦 ADMIN_ONLY→65005 | 无需修复 | | P0-6 | 实现遗漏 | A2-1-1 混合展示 | §7(E) 可配置状态集 | PUBLIC_POOL 硬编码 status=PENDING | **票 07 已修**:可配置状态集 | | P0-7 | 实现遗漏 | A2-1-4/A2-1-4-1 角色分支 | §6.1 grill #6 | createLead 无角色判定,销售可建未分发 | **票 07 已修**:角色分支+选人即分配 | ### P1(8 项) | 编号 | 裁决 | 原型依据 | PRD 依据 | 代码现状(修复前) | 处置 | |---|---|---|---|---|---| | P1-1 | 实现遗漏 | —(PRD §6.4) | §6.4 清除草稿行 | submitFeedback 提交后不清 DRAFT | **票 08 已修**:submitFeedback 提交后清 DRAFT | | P1-2 | 真实缺陷 | A2-1-4"仅管理员"(被 PRD 推翻) | §3.3 管理员OR创建人 | isCreator 死变量+DataScope 兜底假设不成立 | **票 08 已修**:service 显式权限守卫 | | P1-3 | 实现遗漏 | A2-1-2-1 历史记录 | §6.9/§6.10 字段级 diff | editLead 只记 leadName 单值 | **票 08 已修**:字段级 old→new diff | | P1-4 | 误报 | A2-1-2-1"未过期失效可见" | §3.3 矩阵 | guard 白名单已完备(FeedbackCmd) | 无需修复(单测确认) | | P1-5 | 误报 | A7-3-1 联动刷新(前端语义) | §4.1.1 dept_id 跟池走 | PoolChangedEventListener 已实现 | 无需修复(常量修复在票 09) | | P1-6 | 实现遗漏 | A7-3-1"次日起按新规则" | §5.3 刷新公式 | N 重算无代码,M 注释自认预留 | **票 09 已修**:3 条 setSql deadline 重算 | | P1-7 | 真实缺陷 | A2-1-1 实时统计 | ADR-0023 D3 分组计数 | countStats selectList 内存循环 | **票 09 已修**:SQL GROUP BY 聚合 | | P1-8 | 真实缺陷 | —(PRD/ADR 主导) | §6.7+ADR-0019 顺序硬保证 | 两独立 @Scheduled 软间隔竞态 | **票 09 已修**:合并为 LeadMaintenanceJob | ### P2(7 项) | 编号 | 裁决 | 原型依据 | PRD 依据 | 代码现状(修复前) | 处置 | |---|---|---|---|---|---| | P2-1 | 误报 | A2-1-1 防抖(前端) | §6.2 CAS 语义 | applyClaim CAS+checkCas 65010 | 无需修复 | | P2-2 | 误报 | A7-3-1"≥0 整数" | §4.3 默认值 | validatePool 四项全齐(hold≥1→≥0 票 06 修) | **票 10 已修**:注释 ≥1→≥0 | | P2-3 | 误报 | A7-3-1"全局唯一" | — | 唯一校验+@TableLogic+deleteKey | 无需修复 | | P2-4 | 实现遗漏 | A7-3-1"拦截提交" | §9 写权双守 | savePool 无角色判定 | **票 10 已修**:非管理员 deptId/geography 变更→拒绝 | | P2-5 | 实现遗漏* | A7-3-1 批量按钮隔离 | §9 RBAC | 全接口 fail-open | **用户决策 B 关闭**:维持 fail-open | | P2-6 | 误报 | A2-1-1 反馈三态 | — | 快照列+草稿不刷 | 无需修复 | | P2-7 | 误报 | A2-1-2-1 终态收敛 | §6.9+§3.3 | guard+editLead 终态锁完备 | 无需修复 | --- ## 二、修复方案摘要 ### Ticket 05:P0-1 池删除守卫(跨模块 seam) - **方案**:crm-rule 定义 `PoolLeadOccupationPort`(outbound port 接口),crm-lead 实现 `PoolLeadOccupationAdapter`(查 `lead` 表 `status IN (1,2,3,4)`) - **改动文件**:`PoolLeadOccupationPort.java`(新)、`PoolLeadOccupationAdapter.java`(新)、`LeadPoolServiceImpl.java`(注入 ObjectProvider + deletePool 守卫) - **ADR**:ADR-0025(跨模块 port seam);双模块 CONTEXT.md 更新 - **单测**:+4(拒删/放行/port null/批量部分失败) ### Ticket 06:P0-2/3/4/5 额度校验链 - **方案**:新建 `ClaimLimitChecker` 组件——hold 计数 `status IN (3,4)` + daily 计数当日 CLAIM 日志;claimLead/assignToUser/createLead 三入口注入校验 - **用户决策**:hold=0=字面 0(不是无限),validatePool holdLimit≥1→≥0 - **改动文件**:`ClaimLimitChecker.java`(新)、`LeadServiceImpl.java`、`LeadPoolServiceImpl.java` - **单测**:+7(ClaimLimitCheckerTest)+ LeadServiceImplTest 额度边界 ### Ticket 07:P0-6/7 + 选人即分配 - **P0-6**:PUBLIC_POOL 视图从硬编码 `status=PENDING` 改为可配置状态集(`LeadConstants.PUBLIC_POOL_VISIBLE_STATUSES = {2,3,4}`) - **P0-7**:createLead 角色分支——销售强制回填主部门池(无池报错),管理员可指定任意池/未分发 - **选人即分配**:createLead 4 参数重载(+assignToUserId),管理员选人→创建即分配(走额度校验) - **改动文件**:`LeadViewQueryImpl.java`、`LeadServiceImpl.java`、`LeadController.java`、`LeadConstants.java` - **单测**:LeadServiceImplTest +3(主部门无池/管理员建未分发/选人即分配)+ LeadViewQueryImplTest 更新 ### Ticket 08:P1-1/2/3 service 守卫 - **P1-1**:`LeadServiceImpl.submitFeedback` 在 transition 提交成功后,删除同 (user_id, lead_id) 的 DRAFT 行(草稿随提交转为 SUBMITTED,见 LeadServiceImpl L405-408) - **P1-2**:deleteLead 显式权限守卫 `(状态≠CONVERTED) AND (管理员 OR 创建人)`,SecurityUtils.hasRole 判定 - **P1-3**:editLead 对 §6.9 可编辑 13 字段做 old→new diff,写入 history - **改动文件**:`LeadServiceImpl.java`(P1-1 草稿清理在 submitFeedback / P1-2 / P1-3) - **单测**:LeadServiceImplTest 守卫测试 ### Ticket 09:P1-5/6/7/8 联动/Job/统计 - **P1-5**:常量修复 `in(status, 1, 2)` → `in(status, STATUS_UNDISTRIBUTED, STATUS_PENDING)` - **P1-6**:3 条 setSql deadline 重算(CLAIMED→claim_time+N、FOLLOWING→feedback_time+N、非CONVERTED→create_time+M) - **P1-7**:countStats 从 selectList 内存循环改为 3 次 selectMaps + GROUP BY 聚合 - **P1-8**:LeadExpireJob + LeadRecycleJob 合并为 LeadMaintenanceJob(单一 @Scheduled 串行 expire→recycle) - **改动文件**:`PoolChangedEventListener.java`、`LeadViewQueryImpl.java`、`LeadMaintenanceJob.java`(新)、`LeadTransition.java`(javadoc) - **删除文件**:`LeadExpireJob.java`、`LeadRecycleJob.java` - **单测**:+4(PoolChangedEventListenerTest)+1(LeadMaintenanceJobTest)+ LeadViewQueryImplTest 更新 ### Ticket 10:P2-4/2/5 健壮性 - **P2-4**:savePool 角色拦截——非管理员编辑时 deptId/geography 变更→拒绝(CODE_POOL_INVALID),用 Set 集合比较 geography 忽略顺序 - **P2-2**:LeadPoolDTO/LeadPool 注释 ≥1→≥0,0=不可领取(validatePool ≥0 已在票 06 修) - **P2-5**:用户决策 B——维持 fail-open,不种子化 button 权限点(记风险项) - **改动文件**:`LeadPoolServiceImpl.java`、`LeadPoolDTO.java`、`LeadPool.java` - **单测**:+4(P2-4 角色拦截)+ 适配 1(mockStatic SecurityUtils) --- ## 三、回归测试结果 > 详见 [回归测试结果.md](回归测试结果.md) ### 全量单测:BUILD SUCCESS | 模块 | 测试用例数 | Failures | Errors | |---|---|---|---| | crm-base | 33 | 0 | 0 | | crm-rule | 30 | 0 | 0 | | crm-lead | 97 | 0 | 0 | | 全 9 模块合计 | 447+ | 0 | 0 | 前后对比:crm-lead 6→9 测试类(+3 新增),crm-rule LeadPoolServiceImplTest 18→22 用例。 ### BOM 扫描:ALL CLEAN 本 effort 修改的 19 个 .java 文件逐个检查无 BOM。全库扫描因文件量大超时,未修改文件可由既有 `mvn compile` 通过推定无 BOM。 ### 接口模拟:降级为单测覆盖映射 沙箱环境无法启动 Spring Boot 服务。6 个 P0 场景均有对应单测覆盖(详见回归测试结果.md §三)。 > **构建前提**(复核者须知):本 effort 修改了 crm-base 的 `SecurityUtils.hasRole(String)`。若直接 `mvn test -pl crm-rule,crm-lead` 而未先刷新 crm-base 本地仓产物,会报 `NoSuchMethod 'SecurityUtils.hasRole'`(用的是旧 jar)。正确顺序:先 `mvn -pl crm-base install -DskipTests`(或带 `-am` 全链重装),再跑下游模块测试。已验证:刷新后 crm-rule 30 + crm-lead 97 全绿。 --- ## 四、建议后续(不做,只建议) ### 4.1 前端配合点 1. **新增错误码 toast 文案**:65006(每日超限)、65007(持有超限)已激活但前端无对应文案 2. **按钮置灰**:P2-1 防抖/置灰属前端交互(后端 CAS 兜底已存在) 3. **公海列表领取按钮**:仅 `status=待领取` 行可点(后端可配置状态集已支持混合展示,前端需按行状态控制按钮 disabled) 4. **新增线索表单**:管理侧"选人即分配"需前端传 `assignToUserId` 参数;销售侧 poolId 字段需隐藏(后端强制回填主部门池) ### 4.2 Bruno API 文档同步 以下接口行为/错误码在本 effort 发生变化,Bruno collection 需同步: | 接口 | 变化点 | |---|---| | DELETE /pool/{id} | 新增 64005(池下有活跃线索) | | DELETE /pool/batch | 批量删除产出 HAS_ACTIVE_LEAD 失败项 | | POST /lead/claim | 新增 65006/65007(额度超限) | | POST /lead/assign | 新增 65006/65007(被分配人额度超限) | | POST /lead | claimOnCreate 超限时整体失败;新增 assignToUserId 可选参数 | | PUT /pool | 非管理员改归属字段→64001(CODE_POOL_INVALID) | ### 4.3 风险项 1. **P2-5 fail-open 风险**:批量删除/批量分配等接口无后端角色校验(用户决策 B 维持),可被绕过前端隐藏直接调接口。建议后续在 PermissionInitializer seam 补齐 button 权限点种子时统一收口。 2. **集成环境验证缺口**:JWT 认证链路、@DataScope 实际 SQL 注入、PoolChangedEvent 跨模块事件、@Scheduled 定时触发均未在集成环境验证(单测用 mock 模拟)。 ### 4.4 文档更新 - ADR-0025(跨模块 port seam)已落 `docs/adr/` - crm-rule / crm-lead CONTEXT.md 已更新(ticket 05) - 建议 PRD v1.3 补录:管理侧"选人即分配"流程(A2-1-4-1 原型有页、PRD §6.1 未定义,本 effort 按原型+用户拍板实现)