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.
 
 
 
 
 

4.8 KiB

status
accepted

商机建档深模块:OpportunityIntake(收敛两条创建入口 + 修正转商机漏填 opp_source)

Context

架构审查(/improve-codebase-architecture,2026-08-25,候选 #1):商机建档有两条入口——

  1. 线索侧转商机 port OpportunityCreationPortImpl(crm-lead 出站契约 OpportunityCreationPort 的实现,票 11/12/13/16);
  2. 商机侧直接新建 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 漏进领域内部)。
  • 事务位置@Transactionalopen(),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@Serviceopen()@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)、#3(DisplayNameEnricher)、#4(LeadOpportunityConverter)、#5(oplog recorder)不在本票范围;将来若抽 oplog recorder,writeInitialOplog 是第一候选。
  • 决策来源:/improve-codebase-architecture 审查(2026-08-25,候选 #1)+ grilled consensus;实现接力见 handoff opportunity-intake-20260825