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.

121 lines
11 KiB

2 weeks ago
# BatchRunner:跨模块批量编排执行器 Spec
Status: resolved
> 2026-08-20 执行完毕:单步一票交付落地并验证(全模块 9 模块 BUILD SUCCESS、BOM 0、变更限定 crm-base/crm-lead/crm-rule + docs/adr/0028 + CONTEXT.md ×3 + 本 spec)。
> 落地形态:crm-base `domain.result.BatchRunner`(静态 `run(ids, op, bizFail, unknownFail)`,54 行)+ `BatchRunnerTest`(5 用例锁泛型协议);
> crm-lead 8 个批量方法一行委派 + `runBatch` 私有方法删除 + `LeadBatchFailItem.bizOf/unknownOf`;crm-rule `deletePoolBatch` 19 行内联 → 1 行 + `PoolBatchFailItem.bizOf/unknownOf`。
> 测试对账:crm-base 33 + 5 = 38(GlobalDataBindingAdviceTest 4 + BatchResultTest 4 + BatchRunnerTest 5 + DataVisibilityTest 20 + PageConverterTest 5);crm-lead 116 不变;crm-rule 30 不变。
> 两侧存量测试**零改动**(LeadBatchServiceTest 7 / LeadPoolBatchDeleteTest 4 / LeadPoolServiceImplTest P0-1)——自动升级为真实执行器端到端锁;两个 ServiceImpl 构造器零变化(11-null / 6-null 不动,红线达成)。
> 过程插曲:召回了一条错误记忆(声称本票已完成)——经文件系统验证为过早完成态幻觉,已纠正后开工。
> 第三轮架构审查(2026-08-20)候选 ③「批量操作编排重复」(Strong)。把「逐条委派 + 失败翻译收集 + 结果组装」的批量编排皮从 crm-lead `LeadServiceImpl.runBatch`(私有)与 crm-rule `LeadPoolServiceImpl.deletePoolBatch`(内联展开)两处同构实现收进 crm-base 的泛型静态执行器 `BatchRunner`。纯重构:HTTP 契约、失败映射真值、self-injection 事务契约零变化。
>
> 域:crm-base(产出)+ crm-lead / crm-rule(两侧接线)。前置:ADR-0023(批量操作 D1/D2/D5——BatchResult 骨架 + 各域自持失败项 + 依赖方向约束)。本 spec 只重构编排知识的归属,不新增业务。
> 单步一票交付,随票落 ADR-0028 + 三处 CONTEXT.md 增量。
---
## Problem Statement
作为维护者,批量编排(空判 → 逐条委派 self 代理 → catch 业务异常按 code 翻译 → catch 未知异常 UNKNOWN 兜底 → 结果组装)有两份同构实现:
- crm-lead `LeadServiceImpl.runBatch`(私有,19 行):8 个批量方法一行委派它(claim / assignToPool / assignToUser / release / activate / delete / follow / unfollow)。
- crm-rule `LeadPoolServiceImpl.deletePoolBatch`(内联展开,19 行):同一 try-catch 结构逐字重复,差异仅类型(`PoolBatchFailItem`/`PoolBatchFailReason`)与 log 文案。
后果:
- **P1-6 模式本体**:改兜底行为 / 映射结构要人肉排查两个模块,两份实现会漂移。
- **ADR-0023 D5 已预言**「此模式作为后续任何模块批量接口的范式」——但范式没有执行器载体,后续域(商机)只能再抄第三份。
- runBatch 住在大类里(`LeadServiceImpl` 633 行),批量测试靠 `new LeadServiceImpl(11 个 null)` + `setField(self)` 脆弱构造。
事实修正(相对最初指令):Controller 侧无编排逻辑(纯 `Result.success` 透传);批量不直接调 CAS 守卫(守卫在单条方法与状态机内),收的是「逐条委派 + 失败收集 + 结果组装」编排皮;指令的 `com.crm.lead.batch` 收不齐跨模块重复(ADR-0023 D5 依赖方向禁止 crm-rule→crm-lead)。
## Solution
作为开发者,我希望 crm-base 提供泛型静态执行器,使:
```java
package com.crm.base.domain.result;
public final class BatchRunner {
public static <F> BatchResult<F> run(List<Long> ids, Consumer<Long> op,
BiFunction<Long, BusinessErrorException, F> bizFail,
BiFunction<Long, Exception, F> unknownFail) {
// 空判 → for → op.accept → addSuccess
// catch BusinessErrorException → bizFail.apply(id, e) → addFailure
// catch Exception → log + unknownFail.apply(id, e) → addFailure(不中断后续)
}
}
```
调用方(crm-lead,8 个方法形态统一):
```java
public BatchResult<LeadBatchFailItem> claimBatch(List<Long> ids) {
return BatchRunner.run(ids, self::claimLead, LeadBatchFailItem::bizOf, LeadBatchFailItem::unknownOf);
}
```
翻译工厂落各域失败项类(域内知识,兜底文案自持):
```java
// LeadBatchFailItem(crm-lead)
public static LeadBatchFailItem bizOf(Long id, BusinessErrorException e) {
return of(id, LeadBatchFailReason.fromCode(e.getCode()), e.getMessage());
}
public static LeadBatchFailItem unknownOf(Long id, Exception e) {
return of(id, LeadBatchFailReason.UNKNOWN, "操作失败");
}
// PoolBatchFailItem(crm-rule)同款,兜底文案「删除失败」
```
## User Stories
1. 作为 crm-base 维护者,我希望 `BatchRunner.run` 独家提供「逐条委派 + 失败收集 + 结果组装」编排,以便批量协议(结果骨架 + 执行器)在 `domain.result` 一个包讲完。
2. 作为 crm-lead 维护者,我希望 8 个批量方法全部一行委派 `BatchRunner.run(ids, self::xxx, LeadBatchFailItem::bizOf, LeadBatchFailItem::unknownOf)`,以便 `runBatch` 私有方法删除、`LeadServiceImpl` 瘦约 57 行、**构造器零变化**。
3. 作为 crm-rule 维护者,我希望 `deletePoolBatch` 的 19 行内联编排缩成一行委派(翻译工厂换 `PoolBatchFailItem`),以便 crm-rule 不再持有第二份编排实现。
4. 作为两个域的维护者,我希望 code→reason 映射与兜底文案留在各自失败项类(`bizOf`/`unknownOf` 静态工厂),以便 ADR-0023 D5「各域自持失败语义」原样成立。
5. 作为调用方,我希望 `BatchRunner` 是静态方法而非 bean,以便两个 ServiceImpl 构造器零变化、现有测试的 mock-self 模式原样保留。
6. 作为后续域(商机)维护者,我希望将来的批量接口直接 `BatchRunner.run(...)` + 自建失败项,以便第三份编排实现永远不会诞生。
7. 作为维护者,我希望 self-injection(`@Lazy` self 走 AOP 每条独立事务)留守两侧 service,以便 ADR-0023 D1 事务契约不动。
8. 作为测试维护者,我希望两侧现有批量测试(`LeadBatchServiceTest` 7 用例 + `LeadPoolBatchDeleteTest` 4 用例 + `LeadPoolServiceImplTest` P0-1)**一行不动**,以便它们自动升级为「真实静态执行器 + 域翻译工厂」的端到端锁(HTTP 契约直接投影)。
9. 作为 crm-base 测试维护者,我希望新建 `BatchRunnerTest` 以简单 F(String)锁泛型编排协议——空 ids 不触碰 op、bizFail 收 `(id, e)`、unknownFail 兜底、单条异常不中断后续、`total = successCount + failCount`——以便协议行为与域无关地锁定。
10. 作为维护者,我希望对外 HTTP 契约(9 个 `-batch` 端点 URL/入参/返回结构)、失败映射真值、`BatchResult` 结构全部不变,以便这是一次可对账的纯重构。
11. 作为维护者,我希望执行器内 log 统一通用文案(如「批量操作单条异常:id={}」),以便 log 不再各域重复——这是唯一可接受的行为微调(非契约)。
## Implementation Decisions
- **落位**:`com.crm.base.domain.result.BatchRunner`(与 `BatchResult` 同包——批量协议家族);静态 final 工具类、私有构造器;非 bean(零依赖无状态——一个 adapter 是假 seam,无第二实现预期)。
- **签名**:`run(ids, op, bizFail, unknownFail)` 四参数;`bizFail = (Long, BusinessErrorException) -> F`、`unknownFail = (Long, Exception) -> F`。**不造 BatchAction record**——op 每调用点必变,打包零复利(推翻最初指令的 record 预期,如实记录)。
- **翻译工厂归属**:各域失败项类加 `bizOf`/`unknownOf` 静态工厂;兜底文案(lead「操作失败」/ pool「删除失败」)由域自持——行为零变化。
- **方法名**:`run`(`runBatch` 的自然延续,读作 `BatchRunner.run`)。
- **replace 不 layer**:`runBatch` 私有方法删除、`deletePoolBatch` 内联编排删除,不留旧路径。
- **单步一票**:改动面(crm-base 2 新文件 + crm-lead 2 类小改 + crm-rule 2 类小改 + 0 个存量测试改造)小于 LeadDeadlines 单步先例;crm-base 新增与两侧接线强耦合(建了不接线 = dead code 入库),无有意义的中间态。
- **交付物**:本票代码 + `docs/adr/0028-batch-runner-unified-executor.md` + 三处 CONTEXT.md 增量(crm-base「批量执行器」新词条、crm-lead「批量结果」词条更新、crm-rule 批量新词条)。
- **与 ADR-0023 的关系**:D2「BatchResult 放 crm-base 保持非业务纯净」原样成立——BatchRunner 泛型无业务枚举依赖(映射经函数参数注入);D5「各域自持失败语义」原样成立——枚举/失败项不动、只加翻译工厂;D1 事务契约不动(self 留调用方)。
## Testing Decisions
- **两侧现有测试零改动**:静态执行器被真实穿过;它们锁的「code→reason→failures 结构穿全链路」恰是 HTTP 契约的直接投影。**不套用**候选 ② P1-6 平移先例——那是因为被测对象换家,本轮 service 方法没换家。
- **新建 `BatchRunnerTest`**(crm-base,与 `BatchResultTest` 同包):String 作 F 锁协议矩阵——空/null ids 不触碰 op 且空结果、全成功计数、bizFail 收 `(id, e)` 产出失败项、未知异常走 unknownFail 且不中断后续、total 组装。
- **对账公式**(写进执行记录):crm-lead 116 不变;crm-rule 不变;crm-base 基线 + `BatchRunnerTest` 新增约 5-6(基线用例数跑时确认)。
- **prior art**:`BatchResultTest`(crm-base 测试先例)、`LeadBatchServiceTest`(mock self 模式)、`LeadPoolBatchDeleteTest`(同构)。
## Out of Scope
- 两侧 ServiceImpl 的 self-injection / 构造器 / 其他方法。
- `LeadBatchFailReason` / `PoolBatchFailReason` 枚举与 `fromCode` 映射逻辑。
- `BatchResult` 结构与 crm-base 其他通用件。
- HTTP 契约、错误码、DB schema、Redis。
- 第三轮审查候选 ④(V-CONFIG)。
- `LeadServiceImpl` 11-null 测试构造的治理(另一重构主题——本轮静态方法红利只是不再恶化)。
- 商机模块批量接口(尚不存在,将来复用)。
## Further Notes
- **预防项**:两个 ServiceImpl 构造器**预期零变化**——diff 出现构造器改动即为偏离信号;新文件 BOM 扫描;测试名跟被测类名 `BatchRunnerTest`;mvn `-s settings.xml` 且输出落盘再读(`-q` 吞输出)。
- **收尾统一**:crm-base + crm-lead + crm-rule 三模块 mvn test(或直接全模块)→ BOM 扫描 → git 变更清单核对(预期限定 crm-base / crm-lead / crm-rule + `docs/adr/0028` + CONTEXT.md ×3 + `.scratch/batch-runner-module/`)。
- **事实勘察修正记录**(相对最初指令):Controller 无编排逻辑;「逐条 CAS 守卫调用」实为「逐条委派 self 单条方法」(守卫在单条方法与状态机内);`com.crm.lead.batch` 因依赖方向收不齐跨模块重复。