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.deadline(LeadDeadlines接口 +DeadlineBudgetrecord 在包根)+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(grepnew 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
- 作为 crm-lead 维护者,我希望「进入/维持私海的 N 预算」由
recycleOnPrivateEntry(pool, anchor)独家提供,以便领取(claimLead)、分配(assignToUser 私海分支)、作废恢复(submitFeedback 有效分支)、创建即领取(applyClaim)四处共享同一公式与无池防御。 - 作为维护者,我希望 M 锚创建时刻的规则由
expireFromCreation(createTime, pool)显式命名,以便「创建时now + M与 assignToPool 的createTime + M是同一规则」从隐性认知变为显式契约。 - 作为维护者,我希望激活的全部条件(失效前态是否私海决定 N、有无池决定 M 重锚还是保留原值、
statusBeforeExpire为 null 时默认待领取)收进budgetForActivation(lead, pool, now),以便activateLead的条件计算缩成一行调用。 - 作为维护者,我希望池 N/M 变更后的存量重算(§5.3:CLAIMED 锚 claim_time、FOLLOWING 锚 feedback_time、M 锚 create_time 且排除已转商机)由
refreshForPoolChange(poolId, pool)承接,以便DATE_ADD内联 SQL 拼接离开 listener,批量锚点规则与单行版同家。 - 作为调用方(
LeadServiceImpl.claimLead),我希望领预算变成leadDeadlines.recycleOnPrivateEntry(pool, now).recycle(),以便方法体只剩编排(取池、额度校验、快照、迁移)。 - 作为调用方(
assignToUser),我希望起始态分支(私海分支调预算、作废分支不调)留在 service——那是状态机语言;模块只收「只有计时器才需要做的判断」。 - 作为调用方(
submitFeedback),我希望仅有效反馈分支调recycleOnPrivateEntry,无效反馈(作废)路径不设预算的原语义保持。 - 作为调用方(
assignToPool),我希望传lead.getCreateTime()给expireFromCreation,以便锚创建时刻的语义由方法名表达而非调用处约定。 - 作为调用方(
LeadCreationPlannerImpl),我希望applyPoolBinding/applyClaim的两处plusDays改调新模块,以便创建路径与流转路径共享同一预算来源。 - 作为调用方(
PoolChangedEventListener),我希望瘦成纯编排(刷池名快照 + 刷 dept + 委派refreshForPoolChange),以便非计时器知识(§4.1.1 快照/dept 刷新)与计时器知识分家。 - 作为维护者,我希望
DeadlineBudget统一作为预算返回类型,以便「预算」有单一词汇,未来「一次事件动两个 deadline」的新边(现仅激活)直接支持。 - 作为维护者,我希望
TransitionCmd的 7 个 record 与LeadTransitionImpl/LeadMaintenanceJob零改动,以便 ADR-0021 的 cmd 预计算契约原样成立(错的只是预计算无家,不是契约本身)。 - 作为维护者,我希望对外 HTTP 契约、DB schema、Redis、错误码全部不变,行为语义逐场景等价(含 assignToPool 锚 createTime、激活无池保留原 M、作废分支 N=null 等微妙语义原样平移),以便这是一次可对账的纯重构。
Implementation Decisions
- 边界判据:「调用方本来就要做的判断不收」(如 assignToUser 按起始态走分支——状态机语言);「只有计时器才需要做的判断收」(锚点选择、激活条件、无池防御)。
- #8 停止规则留守:
applyRelease/applyFeedback(作废)/executeRecycle里清recycleDeadline=null与清 owner/claimTime 是同一 CAS 语句的同一批字段侧效,不拆;规则文字进LeadDeadlinesjavadoc 规则表。 - #9 到期扫描不动:
executeRecycle/executeExpire+LeadMaintenanceJob是 ADR-0019(失效先/回收后)+ ADR-0021 属地,搬家零收益。 - #10 批量刷新收执行:
refreshForPoolChange直接跑 2 段 update(注入LeadMapper),listener 剩快照/dept 两段。接受 interface 混合「纯预算 + 写路径」两种笔法,方法命名泾渭分明(budget/recycle/expirevsrefresh)。 - 锚点规则表(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(接口 +DeadlineBudgetrecord 在包根)、deadline/impl/LeadDeadlinesImpl、测试deadline/impl/LeadDeadlinesImplTest——与creation包(ADR-0026)及四机制包同构。
Testing Decisions
- 真值集中模块测试:
LeadDeadlinesImplTest锁规则矩阵——预算方法断言DeadlineBudget等值(激活的失效前态×有无池×私海/公海组合、statusBeforeExpirenull 默认、无池防御);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 art:
PoolChangedEventListenerTest(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实时算 vsrecycle_deadline字段——另案)。 - 第三轮审查候选 ③(BatchRunner)/ ④(V-CONFIG)。
Further Notes
- 预防项:
LeadServiceImpl构造器加LeadDeadlines参数后,全模块 grepnew LeadServiceImpl((先例:LeadBatchServiceTest手动 new 会编译失败);批量写文件后 BOM 扫描;测试名跟被测类名LeadDeadlinesImplTest;mvn -s settings.xml且输出落盘再读(-q吞输出)。 - 收尾统一:crm-lead 全量测试 → 全模块
mvn test→ BOM 扫描 → git 变更清单核对(预期全部限定 crm-lead + docs/adr + CONTEXT.md)。 - 编辑落盘假成功先例(DebugTokenController):磁盘时间戳 + mvn 交叉验证。