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.
 
 
 
 
 
 

12 KiB

LeadDeadlines:线索双计时器规则深模块 Spec

Status: resolved

2026-08-20 执行完毕:单步一票交付落地并验证(crm-lead 116 测试全绿、全模块 9 模块 BUILD SUCCESS、BOM 0、变更限定 crm-lead + docs/adr/0027 + crm-lead/CONTEXT.md + 本 spec)。 深模块落位:com.crm.lead.deadlineLeadDeadlines 接口 + DeadlineBudget record 在包根)+ deadline/impl/LeadDeadlinesImpl。 接线 7 处:LeadServiceImpl 五处(claimLead / assignToPool / assignToUser / submitFeedback / activateLead——激活十几行条件缩成一行)+ LeadCreationPlannerImpl 两处(applyPoolBinding / applyClaim); PoolChangedEventListener 瘦成纯编排(快照 + dept 两段留下,deadline 三段 SQL 委派 refreshForPoolChange)。 测试对账:106(基线)+ 12(LeadDeadlinesImplTest:8 预算矩阵 + 4 批量重算含 P1-6 三用例平移)− 2(listener 4→2,迁走三用例新增一委派 verify)= 116,与 mvn 实测吻合; 调用方 mock 接线(三方法 lenient stub 防默认 null NPE),激活用例从 isNotNull 升级为 stub 等值断言。 预防项命中一次:LeadBatchServiceTest 构造器 10 null → 11 null(grep new LeadServiceImpl()。

第三轮架构审查(2026-08-20)候选 ②「线索计时器」(Strong)。把双计时器(N=回收天数 / M=失效天数)的起算、重置、条件、锚点规则从 4 个生产文件 10 个场景收进 com.crm.lead.deadline 包的 LeadDeadlines 深模块。纯重构,不改任何对外 HTTP 行为、DB schema、状态机契约。

域:crm-lead。前置:ADR-0021(状态机 CAS + cmd 预计算契约)、ADR-0026(creation 包先例)、P1-6(池变更 deadline 重算已修)。本 spec 只重构预计算知识的归属,不新增业务。 单步一票交付,随票落 ADR-0027 + crm-lead/CONTEXT.md 增量。


Problem Statement

作为维护 crm-lead 的开发者,双计时器的规则知识无家可归:plusDays(pool.getRecycleDays()) 重复 5 处、plusDays(pool.getExpireDays()) 重复 3 处,散布在 LeadCreationPlannerImpl(2 处)、LeadServiceImpl(4 处方法体)、PoolChangedEventListener(3 段内联 DATE_ADD SQL)。后果:

  • 锚点等价性无处记载:创建归池写 now + M、assignToPool 写 createTime + M——两处写法不同但语义相同(M 从创建起跑,创建时 now ≈ createTime),这个等价性只活在读代码的人脑子里。
  • 条件规则只活在调用方:激活的「失效前态是否私海决定 N、无池保留原 M」十几行条件计算躺在 activateLead;assignToUser/submitFeedback 的预算条件各自为政。
  • 改一条计时规则要人肉排查 4 个文件——正是 P1-6 类缺陷的诞生模式(规则的两处实现漂移)。
  • 池变更重算公式内联在 SQL 字符串拼接里("... INTERVAL " + days + " DAY"),与单行 Java 版是同一份锚点知识的两个投影,却互不引用。

Solution

作为开发者,我希望新建深模块 LeadDeadlines(小接口 4 方法、implementation 吸收全部规则矩阵),使:

package com.crm.lead.deadline;

public interface LeadDeadlines {
    /** 进入/维持私海(领取、分配、作废恢复、有效反馈):N = anchor + pool.recycleDays */
    DeadlineBudget recycleOnPrivateEntry(LeadPool pool, LocalDateTime anchor);

    /** 归池(创建/assignToPool):M = createTime + pool.expireDays —— M 锚创建时刻 */
    DeadlineBudget expireFromCreation(LocalDateTime createTime, LeadPool pool);

    /** 激活:M 重锚 now;N 视失效前态是否私海;无池时保留原值 */
    DeadlineBudget budgetForActivation(Lead lead, LeadPool pool, LocalDateTime now);

    /** 池 N/M 变更 → 分状态批量重算(§5.3 公式),返回重算行数 */
    int refreshForPoolChange(Long poolId, LeadPool pool);
}

DeadlineBudget 是 record(recycle / expire 两字段),与接口同包,按事件自然填充(用不到的为 null);生命周期止于调用方——解构后填 TransitionCmd 的既有散字段,不穿越状态机 seam。

User Stories

  1. 作为 crm-lead 维护者,我希望「进入/维持私海的 N 预算」由 recycleOnPrivateEntry(pool, anchor) 独家提供,以便领取(claimLead)、分配(assignToUser 私海分支)、作废恢复(submitFeedback 有效分支)、创建即领取(applyClaim)四处共享同一公式与无池防御。
  2. 作为维护者,我希望 M 锚创建时刻的规则由 expireFromCreation(createTime, pool) 显式命名,以便「创建时 now + M 与 assignToPool 的 createTime + M 是同一规则」从隐性认知变为显式契约。
  3. 作为维护者,我希望激活的全部条件(失效前态是否私海决定 N、有无池决定 M 重锚还是保留原值、statusBeforeExpire 为 null 时默认待领取)收进 budgetForActivation(lead, pool, now),以便 activateLead 的条件计算缩成一行调用。
  4. 作为维护者,我希望池 N/M 变更后的存量重算(§5.3:CLAIMED 锚 claim_time、FOLLOWING 锚 feedback_time、M 锚 create_time 且排除已转商机)由 refreshForPoolChange(poolId, pool) 承接,以便 DATE_ADD 内联 SQL 拼接离开 listener,批量锚点规则与单行版同家。
  5. 作为调用方(LeadServiceImpl.claimLead),我希望领预算变成 leadDeadlines.recycleOnPrivateEntry(pool, now).recycle(),以便方法体只剩编排(取池、额度校验、快照、迁移)。
  6. 作为调用方(assignToUser),我希望起始态分支(私海分支调预算、作废分支不调)留在 service——那是状态机语言;模块只收「只有计时器才需要做的判断」。
  7. 作为调用方(submitFeedback),我希望仅有效反馈分支调 recycleOnPrivateEntry,无效反馈(作废)路径不设预算的原语义保持。
  8. 作为调用方(assignToPool),我希望传 lead.getCreateTime()expireFromCreation,以便锚创建时刻的语义由方法名表达而非调用处约定。
  9. 作为调用方(LeadCreationPlannerImpl),我希望 applyPoolBinding/applyClaim 的两处 plusDays 改调新模块,以便创建路径与流转路径共享同一预算来源。
  10. 作为调用方(PoolChangedEventListener),我希望瘦成纯编排(刷池名快照 + 刷 dept + 委派 refreshForPoolChange),以便非计时器知识(§4.1.1 快照/dept 刷新)与计时器知识分家。
  11. 作为维护者,我希望 DeadlineBudget 统一作为预算返回类型,以便「预算」有单一词汇,未来「一次事件动两个 deadline」的新边(现仅激活)直接支持。
  12. 作为维护者,我希望 TransitionCmd 的 7 个 record 与 LeadTransitionImpl/LeadMaintenanceJob 零改动,以便 ADR-0021 的 cmd 预计算契约原样成立(错的只是预计算无家,不是契约本身)。
  13. 作为维护者,我希望对外 HTTP 契约、DB schema、Redis、错误码全部不变,行为语义逐场景等价(含 assignToPool 锚 createTime、激活无池保留原 M、作废分支 N=null 等微妙语义原样平移),以便这是一次可对账的纯重构。

Implementation Decisions

  • 边界判据:「调用方本来就要做的判断不收」(如 assignToUser 按起始态走分支——状态机语言);「只有计时器才需要做的判断收」(锚点选择、激活条件、无池防御)。
  • #8 停止规则留守applyRelease/applyFeedback(作废)/executeRecycle 里清 recycleDeadline=null 与清 owner/claimTime 是同一 CAS 语句的同一批字段侧效,不拆;规则文字LeadDeadlines javadoc 规则表。
  • #9 到期扫描不动executeRecycle/executeExpire + LeadMaintenanceJob 是 ADR-0019(失效先/回收后)+ ADR-0021 属地,搬家零收益。
  • #10 批量刷新收执行refreshForPoolChange 直接跑 2 段 update(注入 LeadMapper),listener 剩快照/dept 两段。接受 interface 混合「纯预算 + 写路径」两种笔法,方法命名泾渭分明(budget/recycle/expire vs refresh)。
  • 锚点规则表(javadoc + ADR-0027 核心内容):
    • N(回收):锚 = 最近持有活动时刻(claimTime / feedbackTime);存在性 = 私海态(已领取/跟进中);停止 = 释放/回收/作废清 null,转商机冻结不清(扫描范围排除)。
    • M(失效):锚 = 创建时刻(唯一例外:激活重锚 now);存在性 = 非终态;停止 = 转商机/已失效(扫描范围排除);作废态继续跑。
  • replace, don't layer:5 处 plusDays 调用点全部改掉,不留旧路径;无旧类删除(replace 的对象是散布的计算,非模块)。
  • 单步一票交付:改动面(1 新模块 + 5 调用点 + listener 瘦身 + 测试迁移)小于 TokenService 拆分(3 新模块 + 删旧类),无自然切缝,两票编排成本大于收益。
  • 交付物:本票代码 + docs/adr/0027-lead-deadlines-deep-module.md(痛点、边界判据、锚点规则表、与 ADR-0021 关系)+ crm-lead/CONTEXT.md 机制清单加 deadline 包条目。
  • 包布局com.crm.lead.deadline(接口 + DeadlineBudget record 在包根)、deadline/impl/LeadDeadlinesImpl、测试 deadline/impl/LeadDeadlinesImplTest——与 creation 包(ADR-0026)及四机制包同构。

Testing Decisions

  • 真值集中模块测试LeadDeadlinesImplTest 锁规则矩阵——预算方法断言 DeadlineBudget 等值(激活的失效前态×有无池×私海/公海组合、statusBeforeExpire null 默认、无池防御);refreshForPoolChange 沿用现状 captureUpdates 模式断言 getSqlSet()DATE_ADD(claim_time / DATE_ADD(feedback_time / DATE_ADD(create_time、WHERE 排除已转商机、返回行数。
  • P1-6 三用例原样平移PoolChangedEventListenerTest 的 claimedRecycle/followingRecycle/expireDeadline 三用例迁进新家(断言逻辑不变,被测对象换)。
  • 调用方测试 mock 接线LeadServiceImplTest/LeadCreationPlannerImplTest @Mock LeadDeadlines + stub 固定 budget;激活用例从 isNotNull 升级为 stub 等值断言(锁变强);PoolChangedEventListenerTest 剩 P1-5 快照用例 + 新增 verify 委派 refreshForPoolChange(10L, pool)
  • 测试对账公式(写进执行记录):crm-lead 现 106 用例 + LeadDeadlinesImplTest 新增 ~9-11 − PoolChangedEventListenerTest 迁走 2 = ~113-115。
  • prior artPoolChangedEventListenerTest(wrapper sqlSet 断言模式)、LeadCreationPlannerImplTest(纯 ctx 零 mockStatic)、SessionStoreImplTest(深模块行为锁)。

Out of Scope

  • 停止规则/到期扫描的搬家(#8/#9 留守,见 Implementation Decisions)。
  • TransitionCmd / LeadTransitionImpl / LeadMaintenanceJob 的任何改动。
  • HTTP 契约、错误码、DB schema、Redis、PRD 行为语义的任何变更(含「当日不批量清算」口径)。
  • 商机模块计时器与线索的口径统一(last_valid_follow_time 实时算 vs recycle_deadline 字段——另案)。
  • 第三轮审查候选 ③(BatchRunner)/ ④(V-CONFIG)。

Further Notes

  • 预防项LeadServiceImpl 构造器加 LeadDeadlines 参数后,全模块 grep new LeadServiceImpl((先例:LeadBatchServiceTest 手动 new 会编译失败);批量写文件后 BOM 扫描;测试名跟被测类名 LeadDeadlinesImplTestmvn -s settings.xml 且输出落盘再读(-q 吞输出)。
  • 收尾统一:crm-lead 全量测试 → 全模块 mvn test → BOM 扫描 → git 变更清单核对(预期全部限定 crm-lead + docs/adr + CONTEXT.md)。
  • 编辑落盘假成功先例(DebugTokenController):磁盘时间戳 + mvn 交叉验证。