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.
104 lines
12 KiB
104 lines
12 KiB
|
3 weeks ago
|
# 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` 接口 + `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 吸收全部规则矩阵),使:
|
||
|
|
|
||
|
|
```java
|
||
|
|
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 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` 实时算 vs `recycle_deadline` 字段——另案)。
|
||
|
|
- 第三轮审查候选 ③(BatchRunner)/ ④(V-CONFIG)。
|
||
|
|
|
||
|
|
## Further Notes
|
||
|
|
|
||
|
|
- **预防项**:`LeadServiceImpl` 构造器加 `LeadDeadlines` 参数后,全模块 grep `new LeadServiceImpl(`(先例:`LeadBatchServiceTest` 手动 new 会编译失败);批量写文件后 BOM 扫描;测试名跟被测类名 `LeadDeadlinesImplTest`;`mvn -s settings.xml` 且输出落盘再读(`-q` 吞输出)。
|
||
|
|
- **收尾统一**:crm-lead 全量测试 → 全模块 `mvn test` → BOM 扫描 → git 变更清单核对(预期全部限定 crm-lead + docs/adr + CONTEXT.md)。
|
||
|
|
- 编辑落盘假成功先例(DebugTokenController):磁盘时间戳 + mvn 交叉验证。
|