--- 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 漏进领域内部)。 - **事务位置**:`@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`)、#3(DisplayNameEnricher)、#4(LeadOpportunityConverter)、#5(oplog recorder)不在本票范围;将来若抽 oplog recorder,`writeInitialOplog` 是第一候选。 - 决策来源:`/improve-codebase-architecture` 审查(2026-08-25,候选 #1)+ grilled consensus;实现接力见 handoff `opportunity-intake-20260825`。