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.
8.7 KiB
8.7 KiB
转商机 outbound port 契约(crm-lead → 商机模块)
Type: grilling Status: resolved Blocked by: 06
Question
商机模块尚不存在(见 ticket 01)。转商机不能对接现成服务,只能定义 crm-lead 向未来商机模块期望的 outbound port 契约(定期望、非对接实现)。
需在本 ticket 定清:
- 触发条件:仅"跟进中/有效"状态线索可转(对齐 ticket 03 状态矩阵);已转商机线索不可重复转。
- 入参契约:转商机弹窗字段——商机名称(必填)、行业(必填,字典 industry)、甲方(可选)、意向客户(必填)、地区(必填)、备注(可选),加上从线索带出的字段(线索 id、客户信息、需求产品、领取人/部门)。
- port 接口形态:crm-lead 定义一个
OpportunityCreationPort接口(入参 DTO + 返回商机 id),实现由商机模块将来提供;未实现时的降级/占位策略(spec 里标注"待商机模块落地")。 - 线索侧副作用:线索状态 → 已转商机;写一条"转商机"历史记录(含生成的商机名称/id);线索是否仍保留在我的线索列表(原型:已转商机线索仍在列表,仅隐藏释放按钮)。
- 失败/回滚语义:商机创建失败时线索状态是否回滚;幂等(防重复转)。
- 依赖 06 的线索字段模型确定入参映射;依赖 03 的状态矩阵确定触发条件与副作用。
产出:outbound port 接口签名 + 入参 DTO + 副作用与失败语义,写入 ## Answer。属 crm-lead spec 的"对外依赖契约"章节。
Decisions(grill 定稿)
- [Q1=a] 触发条件 = 仅看状态:
status IN (已领取, 跟进中)。反馈情况完全不参与判定。推翻原型"跟进中/已联系/有效"+"禁止:无效/未反馈"口径(产品口径改)。 - [Q2 弹窗字段] 6 字段数据源:
- 商机名称 = 纯文本
- 行业 = 字典 code
opportunity.industry(商机模块将来创建字典分组,crm-lead spec 仅声明依赖) - 甲方 = 纯文本(无客户模块;前端 UX 从线索历史 lead_name 建议候选,最终存文本)
- 意向客户 = 纯文本(同上,无客户模块)
- 地区 =
region_code varchar(12)存国标 code;支持前缀 LIKE 祖先查询(见 02 Amendment) - 备注 = 纯文本
- [Q3=a] port 形态 = 同步接口
OpportunityCreationPort#createOpportunity(cmd) → opportunityId;crm-lead 事务内调用;商机模块未落地时 spec 里注明"接口签名待商机模块实现",运行期无 bean 时报明确错误。 - [Q4=a] 失败/回滚 = 本地事务性回滚(
@Transactional包住"改状态+调 port+写历史",port 抛异常整个事务回滚,线索状态不变)。假设商机模块与 crm-lead 同进程/同库。 - [Q5=a+c] 幂等 = 双保险:
- crm-lead 侧:前置校验
status IN (已领取, 跟进中),已转商机是终态(03 定),重复点第二次天然被拦下(方案 a) - 商机侧:port 契约要求商机表加
UNIQUE(source_lead_id),重复插入自然失败(方案 c)
- crm-lead 侧:前置校验
Answer
一、outbound port 接口签名(crm-lead 定义,商机模块实现)
package com.crm.lead.port.outbound;
/** crm-lead 声明的 outbound port,实现由商机模块提供 */
public interface OpportunityCreationPort {
/**
* 由线索创建商机。
* 同步调用,crm-lead 事务内触发;失败抛异常整个转商机流程回滚。
* 商机侧须保证 UNIQUE(source_lead_id) 幂等语义。
*
* @return 新建商机 id(用于写入 lead_history 详情)
* @throws OpportunityCreationException 商机侧任何失败均抛此异常
*/
Long createOpportunity(CreateOpportunityCmd cmd);
}
二、入参 DTO
public record CreateOpportunityCmd(
// 从弹窗表单
Long sourceLeadId, // 来源线索 id(幂等 key)
String opportunityName, // 商机名称 * 必填
String industryCode, // 行业字典 code (opportunity.industry) * 必填
String partyA, // 甲方 (自由文本, 可空)
String intendedCustomer, // 意向客户 (自由文本) * 必填
String regionCode, // 地区国标 code (varchar 12) * 必填
String remark, // 备注 (自由文本, 可空)
// 从线索带出(crm-lead 服务内组装, 不由前端上送)
String sourceLeadName, // 线索名称快照
String sourcePhone, // 联系电话快照
String sourceProductCode, // 需求产品 code 快照
Long ownerUserId, // 领取人 = 转商机操作人
Long ownerDeptId, // 领取人所属部门快照
Long poolId // 来源公海池 id
) {}
三、副作用(crm-lead 侧, 本地事务内)
BEGIN TX
1. 前置校验:
- lead.status IN (已领取, 跟进中) 否则抛 IllegalStateException("状态不允许转商机")
- 当前用户 = lead.owner_user_id 否则抛权限异常(只有领取人自己能转)
2. 调 port.createOpportunity(cmd) 拿 opportunityId
3. UPDATE lead SET status = '已转商机' WHERE id = ? AND status IN ('已领取','跟进中')
- 行数 = 0 抛并发异常(乐观锁)
4. INSERT lead_history (lead_id, op_type='CONVERT', op_time=now, op_user_id, detail)
- detail 含 { opportunityId, opportunityName }
5. 冻结时效计时器(03 定稿:转商机后 recycle_deadline / expire_deadline 停止判定)
- 实现:定时任务扫描时 WHERE status != '已转商机',无需 UPDATE
COMMIT
四、后置约束(对齐 03 定稿)
- 已转商机 = 终态,锁死所有编辑:不再支持反馈、释放、编辑。
- 已转商机线索仍在"我的线索"列表可见,操作列隐藏【释放】【转商机】【反馈】【编辑】按钮,仅保留【详情】【取消关注】(若已关注)。
- 时效计时器冻结(不再回收、不再失效)。
五、失败语义
| 失败点 | 处理 |
|---|---|
| 前置校验(状态/权限) | 直接拒绝,无副作用 |
| port 调用异常 | 整个事务回滚,线索状态不变,无历史日志 |
| port 成功但后续 UPDATE 失败 | 事务回滚。注意:商机侧因 UNIQUE(source_lead_id) 已插入的记录会被 crm-lead 本地事务回滚看不到——因为若 port 是本地 bean 走同一 DataSource 事务则完全回滚;若 port 跨库/跨进程则需商机侧提供补偿接口(当前假设同库同事务,spec 只留 TODO) |
六、幂等保证
- crm-lead 侧:状态终态保护 + 乐观锁 WHERE 状态条件(第 3 步)。第二次转会因 WHERE 找不到行而抛异常。
- 商机侧:port 契约要求商机表
UNIQUE(source_lead_id),即使 crm-lead 侧防线被绕过,商机侧也保证一线索最多一商机。
七、原型待 UX 修订
产品口径改(已领取 + 跟进中都能转),原型 3 处文案作废:
a2-1-2____.txt:1826:4.转商机:仅有效 / 跟进中 / 已联系线索可见→ 改为仅已领取 / 跟进中线索可见a2-1-2____.txt:1898:状态为跟进中 / 已联系 / 有效的线索可点击【转商机】按钮→ 改为状态为已领取 / 跟进中的线索可点击【转商机】按钮a2-1-2____.txt:1929-1930:1.可打开弹窗的线索状态:跟进中、已联系、有效→ 改为可打开弹窗的线索状态:已领取、跟进中2.禁止转商机场景:已转商机、过期失效、无效、未反馈→ 改为禁止转商机场景:待领取、未分发、过期失效、线索作废、已转商机(反馈情况不参与判定)
八、依赖声明
- 依赖既存:03(状态触发/终态)、06(线索快照字段)、02 Amendment(sys_region.code)
- 依赖未来:商机模块(
OpportunityCreationPort实现方、opportunity.industry字典分组、opportunity表UNIQUE(source_lead_id)) - crm-lead spec 交付时:
OpportunityCreationPort接口 +CreateOpportunityCmdDTO + 异常类型齐全;实现类不写,商机模块落地时 Spring 扫描注入即可
九、CONTEXT 登记
crm-lead/CONTEXT.md:术语补"转商机 = 单向不可逆终态操作,产出商机 id,写 CONVERT 历史"CONTEXT-MAP.md无需改动(08 属 crm-lead 出站契约,不新增模块)
Status
本 ticket 已定稿可交付 → resolved。