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.
 
 
 
 
 
 

5.1 KiB

12 商机侧新增入口:关联线索校验

Type: task Status: resolved Blocked by: 03

Question

产品明确:商机侧新增商机入口(A3-1-1-2-1,票 03 定的直接创建入口),当用户选商机来源 =「线索转入」 时,需要在表单里选一条【关联线索】。

本票落地这个交互 + 服务端校验规则。不改主表字段——【关联线索】复用票 02 已建的 source_lead_id,只是把这个字段从"仅线索侧 port 能写"扩展到"商机侧新增表单也能写"。

决策(已定,落实现)

  • 字段:复用 source_lead_id(票 02,商机主表已建)。不新建 related_lead_id
  • 唯一性:靠已有 UNIQUE(source_lead_id)(票 02),一条线索只能关联一条商机——同一条 DB 约束覆盖两条入口。
  • 必填触发:仅在商机侧新增入口、用户选 oppSource == "线索转入" 时,source_lead_id 必填。线索侧 port 走的路径 oppSource 由商机侧入口层自动填、source_lead_id 从 cmd 带,不走此校验(票 02)。
  • 候选范围:可关联的线索状态 = 已领取(3) / 跟进中(4)(对齐 crm-lead/service/impl/LeadServiceImpl 中"仅已领取/跟进中线索可转商机"的准入条件,两条入口对称)。
  • 唯一性预检:新增提交前查一次 source_lead_id 是否已被占用;命中则返回业务错误"该线索已关联其他商机",避免依赖 DB 唯一键冲突走错误路径。DB 唯一键仍作最终防线(并发下捕获 → 报错,不做静默返回——这里是商机侧新建,不是 port 的幂等场景)。

落地清单

  1. 商机侧新增商机 API(DTO/Controller/Service):
    • 表单新增【关联线索】下拉字段(前端),后端 DTO 加 sourceLeadId(Long,可空)。
    • 校验规则:oppSource == "线索转入"sourceLeadId 必填;否则可空。
    • 候选下拉的服务端接口:查询 owner_user_id = 当前用户(或按 DataScope 可见) AND status IN (3, 4) AND source_lead_id 未被任何商机占用 的线索。
  2. 命中已占用时:{code: "opp.related_lead.taken", message: "该线索已关联其他商机"} 之类的明确错误码。
  3. 两条入口共用建商机核心逻辑(票 02 定)不变:核心逻辑不关心 source_lead_id 从哪来,只负责 insert + 幂等;候选/校验/联动在入口层做。

后端待做(本轮补,拆于 20260826)

前误纠正:map 曾将本票标“已落”——实为误判:当时只验了转商机 port(OpportunityCreationPortImpl) 路径上的 source_lead_id 预检 + 主表 uk,但本票要求的商机侧直接新建商机入口(A3-1-1-2-1) 后端一行未写 —— 商机侧无任何新建 Controller/Service。产品拍板 A:要现在补。

已备资产(可复用)

  • 主表 source_lead_id 注释已明写“两条入口都可写、NULL 不参与唯一=直接创建可多条” + @Index(unique=true)(已核实)。
  • opp_source_01(线索转入)已在 crm-dict opp_source 分组。
  • Lead 状态常量 STATUS_CLAIMED=3 / STATUS_FOLLOWING=4crm-lead/constant/LeadConstants)。
  • 依赖方向 crm-opportunity → crm-lead 已存在(pom 已核)。

架构拍板(甲,20260826):候选线索查询放 crm-lead

  • ILeadService 新增入站查询方法 listConvertibleLeads(userId):返回该用户 status IN (3,4) 的线索轻量 DTO(id/leadName/phone)——“哪些线索可转商机”的准入规则留在线索域(对齐 LeadServiceImpl 转商机准入)。
  • 未被商机占用”过滤由商机侧补(它本就管 opp 表):拿到候选后剔除 source_lead_id 已在 opportunity 表占用的。

落地清单(后端实现)

  1. crm-leadILeadService.listConvertibleLeads(Long userId) + Impl(status IN (3,4) + owner 过滤),返 ConvertibleLeadView(id/leadName/phone)。
  2. crm-opportunity 新建 API
    • CreateOpportunityRequest DTO:oppSource(必填)/sourceLeadId(可空)/ 商机名/行业/省市/客户 id 等表单字段。
    • POST /api/opportunity Controller + IOpportunityCreateService + Impl。
    • 校验oppSource==opp_source_01(线索转入)→ sourceLeadId 必填;否则可空。
    • 唯一性预检:sourceLeadId 非空时查 opp 表已占用 → 报“该线索已关联其他商机”;DB uk 作并发最终防线。
    • 候选线索下拉接口 GET /api/opportunity/convertible-leads:调 listConvertibleLeads 后剔除已被 opp 占用。
  3. 复用建商机核心逻辑(票 02/16):初始状态/阶段落位/初始日志/团队成员 与转商机 port 一致; 直接创建时 oppSource!=线索转入 则 sourceLeadId=null(不写来源快照)。

前端(本仓库外)

  • 表单【关联线索】下拉交互、客户下拉。后端接口(convertible-leads)就绪后前端接入。

⚠ 依赖

  • 依赖票 03(主表结构,已 resolved)。
  • 不阻塞其他票;实现顺序放到主表 + 状态机(票 04)落地之后。