7.5 KiB
ADR-0022: 线索模块读写分离与深模块抽取——LeadServiceImpl 瘦身
Status
Accepted
Context
LeadServiceImpl 在线索业务持续叠加后长成「上帝类」:530 行、11 个构造依赖、16 个 public 方法,一个类同时承载了状态迁移、历史留痕、四视图查询、展示字段拼装、写侧命令编排。具体摩擦:
- 重复:状态守卫(起始态校验 + 持有人校验)散落在
LeadServiceImpl与LeadTransitionImpl两处共 19 处判定;writeHistory/buildDetail(组装LeadHistory+ kv 序列化为 JSON + 落库)在两个类里逐字复制,唯一差异是 transition 版失败时log.warn而 service 版静默return null。 - 依赖过载:11 个依赖里混着读侧(
sysRegionService、leadHistoryMapper读)、写侧(leadFeedbackMapper、historyRecorder)、以及两个死依赖(leadAttachmentMapper、sysDeptService,声明了从不调用)。 - 测试面模糊:读逻辑(四视图 wrapper、
fillDisplayFields)从未被测过,混在写侧命令测试的同一个类里无从下手。
约束(继承自既有 ADR):LeadTransition 必须保持对 crm-rule / crm-auth 的零依赖契约(ADR-0020/0021 语境);controller 只认 ILeadService 契约,抽取不得改变对外接口。
Considered Options
- 按操作类型横切三分(
LeadQueryService/LeadCommandService/LeadTransitionService)——被否:每块都是「校验+委派」的薄转发层,接口和实现一样厚,只是把 530 行摊成三个 180 行,依赖没减、深度没增。这是「为拆而拆」的 shallow module。 - 一次性大重构——被否:风险集中、难验证。
- 按「深度」抽取证据最硬的接缝,逐个 grill + 验证(采纳):只抽那些能把一整块机制藏在窄接口后、让调用方变简单的接缝;每抽一个跑全量测试 + BOM 扫描;抽完用瘦身后的数据决定是否继续。
Decision
分三轮抽出三个深模块,每个吃掉一整块机制、收走对应依赖:
1. LeadTransition 强化——状态机守卫下沉(候选 1)
散落两处的状态守卫收进 LeadTransition 内的统一 guard(lead, cmd),在 execute 分派前跑一次。每条 TransitionCmd 声明式自带 allowedFromStatuses()(合法起始态集合)与 requiresOwner()(是否要求操作人=领取人,纯代码常量、不建表)。
- 非法起始态 →
CODE_STATUS_NOT_ALLOWED;非领取人 →CODE_NOT_OWNER。守卫在 CAS(ADR-0021)之上给出友好前置,CAS 仍是真正的并发防线。 - 「起始态 × 命令内容」的组合子规则(作废+反馈无效、作废自环换 owner)仍留在各
applyXxx,与目标态计算保持 locality。 - ADMIN_ONLY 池规则、转商机端口调用时序等需要查 crm-rule / port 的前置,留在
LeadServiceImpl,守住LeadTransition的零依赖契约。
2. LeadHistoryRecorder——历史留痕深模块(候选 2)
新建 com.crm.lead.history.LeadHistoryRecorder(接口)+ impl.LeadHistoryRecorderImpl(@Component)。单方法藏起整条机制:
void record(Long leadId, HistoryType type, Long userId, Object... detailKv);
detailKv为 k1,v1,k2,v2… 序列化为 JSON;空 kv 存null(保持历史行为,不写{});序列化失败统一log.warn并存 null(吞并原本一处记日志一处静默的分歧)。- 系统触发的回收/失效传
userId = 0L。 - 仅依赖 crm-lead 内部 mapper/实体/枚举,注入
LeadTransition不破坏其零依赖契约。 LeadServiceImpl与LeadTransition各自的writeHistory/buildDetail删除;LeadTransition由此移除leadHistoryMapper+objectMapper两个依赖,LeadServiceImpl移除objectMapper。
3. LeadViewQuery——线索读侧深模块(候选 3)
新建 com.crm.lead.query.LeadViewQuery(接口)+ impl.LeadViewQueryImpl(@Component),吃下整个读路径:
PageResult<LeadDTO> pageLeads(LeadPageParam param); // 四视图分页 + 展示拼装
LeadDTO getLeadDetail(Long id); // 详情 + 展示拼装
List<LeadHistoryDTO> listHistory(Long leadId); // 历史时间线(VISIBLE_TYPES)
- 独占读依赖:
sysRegionService(省市 code→name)、leadHistoryMapper(历史读)、leadFollowMapper(关注数/是否关注读,与写侧共享同一 mapper bean)。 - 自持
LeadMapper:pageLeads从继承基类的this.page()改为leadMapper.selectPage(...)——读模块不再借BaseServiceImpl的能力。 LeadServiceImpl的三个查询方法瘦成一行委派,ILeadService契约不变,controller 无感。
4. 停在三个深模块,不再拆
拆完后 LeadServiceImpl 只剩写侧命令编排。剩余命令若强按 command 三分只会造 shallow 转发层。唯一还算深的候选 convertToOpportunity(端口协调 + 事务边界 + 幂等前置)目前只有单一调用点、约 45 行,抽独立协调器 earns 不到 keep——推迟到商机模块落地、事务/补偿变复杂时再抽(届时它才够深)。
Consequences
- LeadServiceImpl 瘦身:530 → 437 行,11 → 8 依赖(移走 5:
objectMapper/leadHistoryMapper/sysRegionService+ 2 死依赖leadAttachmentMapper/sysDeptService;新增leadViewQuery),职责收敛为单一的「写侧命令编排」。 - 零依赖契约保持:三个抽取都未让
LeadTransition触达 crm-rule/crm-auth;LeadHistoryRecorder/LeadViewQuery只碰 crm-lead 内部或既有 crm-rule 读服务。 - 对外契约不变:controller 仍只认
ILeadService,三个查询方法签名不动,仅内部委派。 - 测试面清晰化(replace, don't layer):状态守卫真测迁至
LeadTransitionImplTest(7),历史留痕真测在LeadHistoryRecorderImplTest(2),读侧真测新建LeadViewQueryImplTest(4);LeadServiceImplTest只留写侧命令 + 三个「委派验证」用例(44)。全模块 57 测试通过。 - 可换性更好:换历史存储/换读实现只需换对应
*Impl,LeadServiceImpl一行不改;接口+实现分离与既有LeadTransition风格一致,便于 mock。 - 术语沉淀:
crm-lead/CONTEXT.md新增三条 —— 状态机守卫 / 历史记录器 / 线索读侧,各带_Avoid_反例。 - 读侧魔法值清理(抽取后跟进):
LeadViewQueryImpl四视图switch原用字符串字面量("PUBLIC_POOL"等),新建com.crm.lead.domain.enums.LeadViewType枚举承载(与HistoryType同处),含DEFAULT=MANAGE与fromValue(String)兑底(空/非法值回落 MANAGE),switch改为 switch-on-enum 全视图覆盖。wire 契约不变——LeadPageParam.viewType仍为String,前端传值方式不动。 - 预留 scope 常量澄清:
LeadConstants.SCOPE_*(四视图列偏好scope_key)是为「线索列表接入列偏好」预留但尚未接线的域常量。保留不删:归属正确(scope_key 由业务方 crm-lead 约定,crm-preference 仅作不透明存储,见 crm-preference/CONTEXT.md)。依赖方向锁死处理方式:crm-lead → crm-preference单向,故不得让 crm-preference 反向引用本常量(否则成环);仅在常量处添注释讲清预留意图与此约束。 - 决策来源:
improve-codebase-architectureskill 三轮 grill(2026-01);深度判据见codebase-designskill。