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.
 
 
 
 
 
 

5.3 KiB

status
accepted

线索双计时器深模块:LeadDeadlines(收拢 N/M 规则的唯一家)

Context

双计时器(N=回收天数 / M=失效天数)的规则知识散布在 4 个生产文件、10 个场景:

  1. 公式重复plusDays(pool.getRecycleDays()) 重复 5 处(creation ×2、LeadServiceImpl ×4 方法体,其中 3 处公式恒等 now + N)、plusDays(pool.getExpireDays()) 3 处。
  2. 锚点等价性无处记载:创建归池写 now + M、assignToPool 写 createTime + M——两处写法不同但语义相同(M 从创建起跑,创建时 now ≈ createTime),等价性只活在读代码的人脑子里。
  3. 条件规则只活在调用方:激活的「失效前态是否私海决定 N、无池保留原 M」十几行条件计算躺在 activateLead
  4. 批量与单行两套投影:池参数变更重算公式内联在 PoolChangedEventListener 的 SQL 字符串拼接里(DATE_ADD(claim_time, INTERVAL N DAY) ×3 段),与单行 Java 版是同一份锚点知识的两个投影,互不引用。
  5. 缺陷模式:改一条计时规则要人肉排查 4 个文件——P1-6 类缺陷(规则两处实现漂移)的诞生结构。

Considered Options

(grilling 2026-08-20,七问七钉)

  • 痛点定位:主治「规则无家」(locality,选中)/公式重复(轻量值对象工厂)/调用方心智(计算彻底下沉状态机)。用删除测试衡量:删掉公式工厂复杂度几乎不回流;删掉规则模块,10 场景的锚点/条件规则立刻散回 4 文件。
  • 模块边界:收写入侧单行预算 + 池参数变更批量刷新含执行(选中);停止规则留 LeadTransition(清 recycleDeadline 与清 owner/claimTime 是同一 CAS 语句的同一批字段侧效);到期扫描不动(ADR-0019 失效先/回收后 + ADR-0021 属地,搬家零收益)。
  • interface 形状:按锚点语义 4 方法(选中)/按业务事件 6 方法(其中 3 个实现恒等 now + N——shallow 信号,为「未来事件分化」预付接口税;将来真分化时再从锚点方法裂开)/单方法 + ctx(各事件输入天然异构,ctx 成 nullable 堆,且与状态机 cmd 分发重复)。
  • 值对象穿越TransitionCmd 保持 LocalDateTime 散字段(选中——DeadlineBudget 生命周期止于调用方,解构后填 cmd,状态机零改动)/cmd 改携带 DeadlineBudget(动 7 个 record + 6 处 applyXxx + 全部状态机测试,纯美学代价)。
  • 测试分工:模块测试锁规则真值矩阵 + 调用方 mock 接线(选中,interface 即 test surface)/调用方注入真实实现(构造器带 mapper,纯计算场景传 null 味道差)。

Decision

新建 com.crm.lead.deadlineLeadDeadlines 接口(4 方法)+ DeadlineBudget record(recycle / expire 两字段)在包根,impl/LeadDeadlinesImpl 下沉:

  • recycleOnPrivateEntry(pool, anchor)——进入/维持私海的 N 预算(领取、分配、作废恢复、有效反馈共用;pool null 无池防御)。
  • expireFromCreation(createTime, pool)——M 锚创建时刻(创建时 now 与 assignToPool 的 createTime 是同一规则的方法名显式化)。
  • budgetForActivation(lead, pool, now)——激活条件全收(失效前态是否私海决定 N、无池保留原 M、statusBeforeExpire null 默认待领取)。
  • refreshForPoolChange(poolId, pool)——§5.3 三段 DATE_ADD 批量重算(CLAIMED 锚 claim_time、FOLLOWING 锚 feedback_time、非已转商机锚 create_time),返回合计行数。

边界判据:调用方本来就要做的判断不收(assignToUser 按起始态走分支——状态机语言);只有计时器才要做的判断收(锚点选择、激活条件、无池防御)。

ADR-0021 的 cmd 预计算契约不变:错的只是预计算无家,不是契约本身——DeadlineBudget 解构后填 TransitionCmd 既有散字段,TransitionCmd / LeadTransitionImpl / LeadMaintenanceJob 零改动。

Consequences

  • 5 处 plusDays 调用点全部改走新模块(replace 不 layer);activateLead 的十几行条件计算缩成一行;PoolChangedEventListener 瘦成纯编排(快照 + dept 两段留下,deadline 三段委派)。
  • 锚点规则表落在 LeadDeadlines javadoc(N:锚最近持有活动时刻、存在性=私海态、停止=释放/回收/作废清 null 转商机冻结;M:锚创建时刻唯一例外激活重锚 now、存在性=非终态、作废态继续跑)——改一条计时规则只动一处。
  • 测试 replace 不 layer:P1-6 三用例(DATE_ADD 锚点断言)自 PoolChangedEventListenerTest 原样平移至 LeadDeadlinesImplTest(12 用例:8 预算矩阵 + 4 批量重算);调用方测试 mock 接线,激活用例从 isNotNull 升级为 stub 等值断言。crm-lead 106 → 116 用例。
  • interface 混合「纯预算 + 写路径」两种笔法(recycle/expire/budget vs refresh)——方法命名泾渭分明,语义同属「计时器的一生」。
  • 商机模块计时器(last_valid_follow_time 实时算、无 recycle_deadline 字段)与线索的口径统一不在本 ADR 范围(另案)。
  • 决策来源:/improve-codebase-architecture 第三轮审查(2026-08-20,候选 ② Strong)+ grilling 七问;实现票据见 .scratch/lead-deadline-module/spec.md