5.2 KiB
| status |
|---|
| accepted |
商机建档深模块:OpportunityIntake(收敛两条创建入口 + 修正转商机漏填 opp_source)
Context
架构审查(/improve-codebase-architecture,2026-08-25,候选 #1):商机建档有两条入口——
- 线索侧转商机 port
OpportunityCreationPortImpl(crm-lead 出站契约OpportunityCreationPort的实现,票 11/12/13/16); - 商机侧直接新建
OpportunityCreateServiceImpl(票 12)。
两者逐字复制了五步建档机制(主表落库 + applyInitialStage 阶段落位 + linkPrimaryCustomer 客户子表 + writeInitialOplog 初始日志 + insertOwnerTeamMember 团队成员),外加 source_lead_id 唯一性预检与 DuplicateKeyException 转译,仅初始日志文案/op_source 与 oppSource 取值有细微分叉(PROJECT_ROLE_OWNER/ROLE_INTENDED 等常量也是两份)。复制结构已产出一个真实缺陷:线索转商机路径从未给主表设置 oppSource——产品口径「从线索转来的商机,oppSource 默认置『线索转入』」(CONTEXT.md「商机来源」)自始未在 port 侧成立,两条入口行为漂移。
Considered Options
(grilled consensus 2026-08-25,详见报告卡与 handoff)
- 深模块吃哪个入参:新内部契约
OpportunityIntakeSpec(选中——两条入口字段并集,crm-opportunity 私有)/复用CreateOpportunityCmd(那是 crm-lead 的出站契约,直接新建被迫背着别域 DTO)/复用CreateOpportunityRequest(HTTP DTO 漏进领域内部)。 - 事务位置:
@Transactional落open(),adapter 全部去掉(选中——线索侧并入 crm-lead 外层事务靠 REQUIRED 传播,ADR-0020 语义不变)/adapter 各自保留@Transactional(事务边界留在两个调用点,深模块原子性依赖调用方纪律)。 - 异常形状:自有
OpportunityIntakeException不跨 seam、adapter 在边界翻译(选中)/深模块直接抛BusinessErrorException(HTTP 语义漏进模块内部)/深模块抛 crm-lead 契约异常(反向依赖出站契约)。 - 差异承载:
IntakeSource枚举承载初始日志op_source/op_desc+ LEAD_CONVERT 恒定opp_source_01(选中)/spec 布尔标志(isLeadConvert散落判断)/子类多态(两分支而已,过度设计)。
Decision
新建 com.crm.opportunity.intake 包:
OpportunityIntake:接口,单方法Long open(OpportunityIntakeSpec);OpportunityIntakeSpec:record,内部统一契约(不导出);IntakeSource:枚举LEAD_CONVERT/DIRECT,承载初始日志op_source(SYSTEM/USER)、op_desc(由线索转入创建/直接创建商机)与 LEAD_CONVERT 恒定商机来源码(defaultOppSource()=opp_source_01,DIRECT 为 null、由 spec 显式携带用户所选);OpportunityIntakeException:自有领域异常,不跨模块边界;impl/OpportunityIntakeImpl:@Service,open()标@Transactional(rollbackFor=Exception.class),独占五步建档机制 + 唯一性预检(sourceLeadId 非空才检,DIRECT NULL 不参与 UNIQUE)+DuplicateKeyException→领域异常转译;PROJECT_ROLE_OWNER/ROLE_INTENDED常量随之收敛为私有。
两个 adapter 瘦成纯翻译层:port 侧 cmd→spec(source=LEAD_CONVERT,oppSource=枚举默认)+ OpportunityIntakeException→OpportunityCreationException;直接新建侧 request→spec(source=DIRECT,oppSource 用户所选,ownerDeptId=null 走默认模板兜底、与收敛前行为一致)+ OpportunityIntakeException→BusinessErrorException,并保留入口特有的 sourceLeadId 必填校验与 listConvertibleLeads(后者属候选 #2 范围,不动)。
Consequences
- 机制代码两处(180 + 195 行)收敛一处(intake impl 180 行);adapter 只剩翻译(port 59 行、service 99 行,service 因保留候选线索查询稍厚)。port 的依赖从 5 个 bean 降到 1 个。
- BUG 修正随迁:线索转商机路径
oppSource恒落opp_source_01(守护测试open_leadConvert_setsOppSource),产品口径自此在代码成立。 - 测试 replace 不 layer(ADR-0022/0026 先例):机制断言上移
OpportunityIntakeImplTest(12 例,两来源分支全覆盖),adapter 测试只留翻译断言(port 2 例 + service 4 例);删机制用例不降覆盖。 - 术语沉淀:
crm-opportunity/CONTEXT.md新增「商机建档」「进入方式」。 - 候选 #2(
ILeadService泄漏 /ConvertibleLeadCatalogPort)已随后落地(见 ADR-0031)。候选 #3(DisplayNameEnricher)、#4(LeadOpportunityConverter)另行评估。候选 #5(oplog recorder)已被本模块吸收——两条入口原各自手搓的writeInitialOplog(new OpportunityOplog()+ 8 setter)随机制收敛,现仅存OpportunityIntakeImpl.writeInitialOplog一处;报告预判「likely folded into #1」成立,故 #5 不单独立票。将来若跨「建档初始日志 + 状态迁移历史」再抽统一 recorder,本方法仍是第一候选。 - 决策来源:
/improve-codebase-architecture审查(2026-08-25,候选 #1)+ grilled consensus;实现接力见 handoffopportunity-intake-20260825。