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.
 
 
 
 
 
 

11 KiB

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 提供泛型静态执行器,使:

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 个方法形态统一):

public BatchResult<LeadBatchFailItem> claimBatch(List<Long> ids) {
    return BatchRunner.run(ids, self::claimLead, LeadBatchFailItem::bizOf, LeadBatchFailItem::unknownOf);
}

翻译工厂落各域失败项类(域内知识,兜底文案自持):

// 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) -> FunknownFail = (Long, Exception) -> F不造 BatchAction record——op 每调用点必变,打包零复利(推翻最初指令的 record 预期,如实记录)。
  • 翻译工厂归属:各域失败项类加 bizOf/unknownOf 静态工厂;兜底文案(lead「操作失败」/ pool「删除失败」)由域自持——行为零变化。
  • 方法名runrunBatch 的自然延续,读作 BatchRunner.run)。
  • replace 不 layerrunBatch 私有方法删除、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 artBatchResultTest(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 因依赖方向收不齐跨模块重复。