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.
 
 
 
 
 

6.1 KiB

status
accepted

线索模块批量操作与视图统计接口补全(G1–G5)

线索原型(A2-1-1 / A2-1-4 / A7-3-1 + 分配/激活弹窗)要求批量操作顶部统计卡片,但已实现代码只有单条流转、无批量、无统计。本 ADR 把接口完整性复核(grill-with-docs)定稿的 G1–G5 固化为架构决策。范围只含 G1–G5;G6(线索导入/导出)/ G7(线索合并)/ G8(公海池导入/导出)维持既有 Out of scope(见 .scratch/clue-module/map.md)。

决策来源:.scratch/clue-module/线索业务-PRD.md §13(2026 grill-with-docs,决策清单 B1–B9)。

Decisions

D1 批量操作:非原子、逐条 CAS、部分成功

批量领取/分配到池/分配到销售/释放/激活/删除,统一逐条委派给对应单条操作(各自独立事务),任一条失败只记入结果、不拖垮整批。承接 ADR-0021:批量的「失败」= 单条 CAS 行数 0 或单条守卫抛出的业务错误。

  • URL:单条动词 + -batch 后缀(/api/lead/claim-batch 等),与既有短横线风格一致(B4)。
  • 入参:@RequestParam("ids") List<Long>;批量分配双深度——到池带 poolId,到销售带 userId(与单条 assign-pool/assign-user 对称,B5)。
  • 逐条独立事务的落地:LeadServiceImpl@Lazy self-injection 走 AOP 代理调单条方法(沿用 AuthServiceImpl 既有模式),批量方法本身不加 @Transactional

D2 批量返回 Result<BatchResult<F>>(B2/B3)

  • BatchResult<F>(泛型骨架)放 crm-base/domain/result,与 Result/PageResult 并列,只装 total/successCount/failCount/List<F> failures,对失败项内部结构无感知——保持 crm-base 非业务纯净。
  • 线索域失败项 LeadBatchFailItem(leadId + LeadBatchFailReason + message)放 crm-lead
  • LeadBatchFailReason 新造语义枚举ALREADY_CONVERTED / CONCURRENT_MODIFIED / OVER_HOLD_LIMIT / OVER_DAILY_LIMIT / STATUS_NOT_ALLOWED / NOT_OWNER,各带 code 回指 ResultCode 65xxx,B6),供前端按失败类型聚合展示「成功 N 条 / 失败 M 条(各类型明细)」。

D3 视图统计接口 /api/lead/stats(B7/B8)

  • 入参复用 LeadPageParam,返回 Result<LeadStatsDTO>(total/claimed/converted/todayNew/undistributed)。
  • 统计口径 = 当前视图数据集口径,非全库:与 /page 吃完全相同的 viewType + 筛选 + @DataScope 部门天花板,只把「取一页」换成「按 status 分组计数」。接口通用、全量返回 5 个计数,前端按需取。
  • claimed(「已被领取」,展示文案前端渲染)= status IN (已领取 3, 跟进中 4)。

D4 复合卡片下钻:LeadPageParam.status 单值 → statusIn 多值(B9)

统计卡片可点击下钻,把该卡状态条件塞进 LeadPageParam 再调 /page。「已被领取」是 status IN(3,4) 的并集,单值 Integer 表达不了,故 status 升级为 List<Integer> statusIn/page/stats 共用。改动落在接口未联调/未发文档阶段,成本低。

D5 公海池批量删除的失败项归属(跨模块依赖方向约束)

pool/delete-batch 属 crm-rule;但 LeadBatchFailItem/LeadBatchFailReason 在 crm-lead,而 crm-lead 依赖 crm-rule,crm-rule 不能反向依赖 crm-lead(否则成环)。故池批量删除不能复用线索的失败项类型。

决策:crm-rule 自建失败项,复用 crm-base 的 BatchResult<F> 泛型骨架(方案 A)。

  • PoolBatchFailReason(crm-rule 域枚举,各值带 code 回指 RuleConstants 64xxx):NOT_EXIST(64004)、HAS_ACTIVE_LEAD(64005,池下有非终态线索)、外加 UNKNOWN 兜底。
  • PoolBatchFailItem(poolId + PoolBatchFailReason + message)放 crm-rule。
  • pool/delete-batch 返回 Result<BatchResult<PoolBatchFailItem>>,与线索侧对称。
  • 与线索侧完全对称:crm-base 出泛型骨架,各业务域自持失败语义枚举 + 失败项,天然无环。

注:CODE_POOL_HAS_ACTIVE_LEAD(64005) 当前是预留常量——单条 deletePool 尚未实现「池下有非终态线索则拒删」的守卫。本期不补该守卫(grill 决策):PoolBatchFailReason.HAS_ACTIVE_LEAD 同样预留,本期池批量删除实际只会产出 NOT_EXIST/UNKNOWN。理由:该守卫是独立业务规则,且其查询需跨 crm-rule→线索表(又触及 crm-rule 不能依赖 crm-lead 的方向难题),值得单独 ADR/issue 设计查询归属,不在本批量补全范围。枚举值预留不影响契约,后续补守卫时零契约变更。

Considered Options(D5)

  • A crm-rule 自建失败枚举/失败项(选中)PoolBatchFailReason + PoolBatchFailItem,同样装进 crm-base 的 BatchResult<F>)——各域自持失败语义,crm-base 泛型骨架复用,无环;池删除失败原因(池下有非终态线索 vs 不存在)可结构化返回,前端能分类提示。
  • B pool/delete-batch 返回 BatchResult<Long>(failures 只装失败的 poolId,不带原因枚举)——被否:最省但丢失「为何失败」,前端无法分类展示,而「池下有非终态线索」是需要明确提示用户的业务态。
  • C 池批量删除本期不做——被否:原型 A7-3-1 明确有「批量删除公海池」,且 A 成本可控。

Consequences

  • 新增 crm-base 通用件 BatchResult<F>,可被任意业务域批量接口复用。
  • crm-lead 新增 stats 读接口与 6 个 -batch 写接口;LeadPageParam 契约变更(status→statusIn),需同步读侧 wrapper 与相关测试。
  • 批量接口不引入新的上限校验逻辑——OVER_HOLD_LIMIT/OVER_DAILY_LIMIT 枚举值预留给单条操作后续补齐上限校验时自然生效(当前单条 claimLead/assignToUser 尚未实现上限校验,属既有 gap,不在本 ADR 范围)。
  • D5 依赖方向约束:crm-base 出泛型 BatchResult<F>,crm-lead 与 crm-rule 各自持有本域失败项/失败原因枚举,避免跨业务域依赖成环——此模式作为后续任何模块批量接口的范式。