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.

85 lines
6.7 KiB

3 weeks ago
# 02 lead→opp port 消费侧契约落地
Type: grilling
Status: resolved
## Question
商机侧**实现** `crm-lead/port/outbound/OpportunityCreationPort`(线索 ticket 08 已定稿的出站契约),把"线索转商机"这条断线接通。契约既定,本票钉的是**商机侧落地细节**:
- **幂等落点**:`UNIQUE(source_lead_id)` 建在商机主表哪个字段;重复转商机(幂等命中)时返回既有商机 id 还是抛异常。
- **带出快照的落库**:`CreateOpportunityCmd` 的 sourceLeadName/sourcePhone/sourceProductCode/ownerUserId/ownerDeptId/poolId 分别落到商机主表哪些字段;哪些是快照(不随线索变)哪些是引用。
- **事务边界**:port 同步、在线索事务内触发、失败抛 `OpportunityCreationException` 使线索整体回滚——商机侧建表/写子表的哪些步骤必须在这个同步事务内完成,哪些可异步。
- **直接创建商机**入口(跳过线索)与"由线索转入"入口的字段差异:`source_lead_id` 可空?两条入口如何共用建商机逻辑。
- 转商机成功后线索侧写 `lead_history`(转商机)用到的商机 id 由 `createOpportunity` 返回——确认返回值语义。
不改线索侧契约(已定稿);只定商机侧实现规格。
## Answer
四簇全部拍板(grilling,产品逐条确认)。**注**:grill 过程中冒出一处**必须改线索契约**的决策(意向客户下拉选客户 → 带 `customerId`),已越出「不改线索契约」的原定边界,产品明确要求改,见「契约变更」段 + 派生新票。
### 簇 A — 幂等落点
- 幂等键 = 商机主表 `UNIQUE(source_lead_id)`。`source_lead_id IS NULL`(直接创建)不参与唯一约束,可多条并存。
- **幂等命中 = 安静返回已有商机 id**(写法一,真幂等),不抛异常——`OpportunityCreationPort` javadoc 的「幂等语义」即此意。重复转商机(重试)当作成功,返回既有商机 id。
- **并发防护**:两个转商机事务同时命中同一 `source_lead_id`,先查都查不到、都 insert,DB 唯一索引让后提交者抛 `DuplicateKeyException`。商机侧须**捕获唯一键冲突 → 回查既有商机 → 返回其 id**,不外抛。这样「安静返回已有 id」在并发下也成立。
- 返回值语义:`createOpportunity` 返回的 Long = 商机 id(新建或既有),线索侧据此写 `lead_history`(转商机)。
### 簇 B — 带出快照落库(cmd 字段全部落库,无一丢弃)
| Cmd 字段 | 落主表 | 引用/快照 | 说明 |
|---|---|---|---|
| `opportunityName` | 商机名称 | — | 商机自有业务字段 |
| `industryCode` | 行业 | — | 字典码 |
| `partyA` | 甲方 | — | 自由文本 |
| `intendedCustomer` | 意向客户名 | **快照** | 客户名快照(见契约变更:与新 `customerId` 并存,走法 B)|
| `regionCode` | 地区 | — | 国标码 |
| `remark` | 备注 | — | |
| `ownerUserId` | `owner_user_id` | **引用** | 领取人 = 归属本身 |
| `ownerDeptId` | 领取人所属部门 | **快照** | 转商机那刻部门;领取人调岗不变;抛公海清领取人后仍可归档/报表 |
| `sourceLeadId` | `source_lead_id` | **引用**(幂等键) | 来源线索血缘 |
| `sourceLeadName` | 来源线索名快照 | **快照** | |
| `sourcePhone` | 联系电话快照 | **快照** | |
| `sourceProductCode` | 需求产品码快照 | **快照** | |
| `poolId` | 来源公海池 id | **引用/归档** | **存下来**,纯血缘归档、用于后续排查问题;**不参与商机自身公海逻辑**(票 05 另有商机公海规则)|
### 簇 C — 两条入口共用建商机核心逻辑
- 商机侧一个内部建商机核心方法(建主表行 + 初始状态/阶段 + 初始操作日志 + 子表);**线索转入 / 直接创建两条入口共用**,进入平权(票 01)。
- 差异只在入口层(进核心逻辑前):
| 差异点 | 线索转入 | 直接创建 |
|---|---|---|
| `source_lead_id` | 有值 | NULL |
| `oppSource`(商机来源) | 默认「线索转入」 | 用户弹窗选 |
| 来源快照(线索名/电话/产品码/池 id) | 从 cmd 带 | 全空 |
| `owner_dept_id` 快照 | cmd 传的 | 当前用户当前部门 |
| 幂等检查 | 要(查 `source_lead_id`) | 不要 |
- **`oppSource` 默认「线索转入」由商机侧入口层填**(`CreateOpportunityCmd` 无此字段,不回头改线索契约加它)。
- **新商机 = 状态「推进中」+ 阶段「模板第一步」**(阶段模板细节留票 06,本票只定「落第一步」规则)。
- 直接创建入口的完整 API/字段规格留票 03;本票只钉「两条入口共用核心逻辑 + 差异表」这条边界。
### 簇 D — 事务边界 + 转商机时的子表
- **必须同步(线索事务内,失败抛 `OpportunityCreationException` → 线索整体回滚)**:
1. 幂等检查(查 `source_lead_id`
2. 建商机主表行(全部快照字段 + 初始状态/阶段)
3. 写一条初始操作日志(「由线索转入创建」)
4. **建团队成员一条**:领取人 = 团队成员,项目角色 = **商机负责人**
5. **建关联客户一条**:关系标记 **主要意向客户**,`customer_id` = cmd 带来的 `customerId`(引用)+ `intendedCustomer` 客户名快照
- **可异步(事务提交后,失败不回滚线索)**:通知/提醒(「你有新商机」)、建群/推送(本 out of scope)、其他非核心派生数据。
### 契约变更(越出「不改线索契约」原边界,产品明确要求)
- **`CreateOpportunityCmd` 新增 `customerId`(Long,必填)**;`intendedCustomer` 保留为客户名快照(走法 B:引用 + 快照并存,与 CONTEXT.md「关联客户」术语一致)。
- 线索转商机弹窗:**意向客户改为下拉选客户**(选中拿到 `customer_id`)——线索转商机前置要求「必须有意向客户」。
- **项目角色首项**:「销售负责人」→ **「商机负责人」**(改 `crm-opportunity/CONTEXT.md`「团队成员」术语)。
- ⚠ 客户实体归 A4 客户模块(未建)。此变更依赖「线索侧下拉能查到客户列表」的能力落地——契约字段/领域决策本票钉死,**真正改 `crm-lead` 代码 + A4 客户下拉能力另开新票**(`.scratch/opportunity-module/issues/11-customerid-in-create-cmd.md`)。
### 已核实事实
- `OpportunityCreationPort` / `CreateOpportunityCmd` / `OpportunityCreationException` 现存于 `crm-lead/src/main/java/com/crm/lead/port/outbound/`(线索票 08 定稿),返回 `Long` = 商机 id。
- `crm-opportunity/CONTEXT.md` 现存,「关联客户」术语已写「引用 + 快照」模式,与走法 B 一致。