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.
 
 
 
 
 

12 KiB

客户 ↔ 商机硬依赖全景(票 03 附件)

全部结论以 2026-09-11 HEAD(dd6fa42)源码为准;grep/读码实证,未跑 mvn。 模块方向:Maven 单向 opportunity → customer(ADR-0029),跨域交互全部走「接口定义在 crm-customer、crm-opportunity 出 @Primary 实现」的 port seam。

1. 客户详情「关联商机」页签

1.1 端点与链路

  • 端点:GET /api/customer/opportunity/page?id={customerId}[&current=&size=]CustomerDetailController.pageOpportunities,tag A4 客户管理/客户详情/关联商机;BaseParam 分页)
  • 链路:CustomerDetailServiceImpl.pageOpportunities(仅 require 客户存在 67002) → 出站 port crm-customer/.../port/CustomerOpportunityQueryPort.pageByCustomerId → crm-opportunity port/inbound/impl/CustomerOpportunityQueryPortImpl@Primary 接管 NoopCustomerOpportunityQueryPortImpl 兜底,Noop 不删)。

1.2 SQL 真源(OpportunityMapper.pageByCustomer

SELECT o.* FROM opportunity_customer oc
JOIN opportunity o ON o.id = oc.opportunity_id
WHERE oc.customer_id = #{customerId} AND oc.delete_key = 0 AND o.deleted = 0
ORDER BY o.update_time DESC, o.id DESC
  • 关联真源 = opportunity_customer 多对多子表(软删复用键 delete_key;UNIQUE(opportunity_id, customer_id, delete_key))。客户侧表没有任何商机外键
  • 阶段列不在 SQL join:实现层把本页 current_stage_id 集合交 crm-rule describeStages 批量解析(customNodeName 优先、空则 opp_stage 字典名)。

1.3 出参实际列集(record OpportunityItem,恒 7 字段)

# 字段 类型 物理来源 说明
1 id Long opportunity.id 商机ID
2 oppName String opportunity.opp_name 商机名称
3 stageName String crm-rule describeStages(current_stage_id) 当前工作节点展示名(解析失败为 null → 前端显「--」)
4 estimateAmount BigDecimal opportunity.project_amount 预计金额(=主表「项目金额」列换名出参)
5 ownerUserId Long opportunity.owner_user_id 销售负责人ID
6 ownerNameSnapshot String opportunity.owner_name_snapshot 负责人姓名快照
7 updateTime LocalDateTime opportunity.update_time 最近更新时间

1.4 G2「7 vs 13 列」契约漂移定性

  • 原型 13 列(matrix-a4-4 §4.1):商机名称/项目/区域/招标形式/阶段/预计金额/下次跟进/甲方/意向客户/负责人/更新时间…;实现恒返上表 7 字段。
  • 缺列分两类:①项目/区域/招标形式依赖 A5 与商机主表扩展;②下次跟进时间/甲方/意向客户需 opportunity 侧 port 增列——挂账跨模块票(issues-07 §C 定稿已收窄口径,不阻塞验收)。
  • Demo 契约事实 = 7 字段,缺列显「--」,不得假设后端会补。

1.5 既有 demo(.scratch/customer-e2e/demo/index.html L1104-1108)字段漂移

调用端点正确,但画 5 列且字段名错 3 处:

demo 读的字段 实际字段 现象
o.opportunityName || o.name oppName 都不命中 → 回退显 o.id
o.ownerUserName || o.ownerName ownerNameSnapshot 恒「--」
o.statusName || o.status (无此字段,出参无状态列) 恒「--」
o.stageName / o.estimateAmount 同名 正确

新 demo 直接按 1.3 的 7 个真名渲染即可。

1.6 同 port 的守卫方法( Demo 阻断现象的机制)

hasActiveOpportunity(customerId) = OpportunityMapper.countActiveByCustomeropp_status IN (1,2,3)(1 待领取 / 2 推进中 / 3 暂缓中)计数 > 0。已关闭 4 / 已转项目 5 视为终结,不阻断

2. 「新增/编辑客户挂商机」——票面前提修正

  1. customer 侧没有「挂商机」字段与端点Customer 实体、CustomerCreateDTOCustomerUpdateDTO grep opp|opportunity 均 0 命中;POST /api/customer/create / POST /api/customer/edit?id= 字段面与商机无关(edit 且「归属字段不可改」,API-SUMMARY §2.3)。客户↔商机关联唯一写入口在商机侧(opportunity_customer 子表)。
  2. ticket01 的 opp_id 可空化与客户域无关:两次提交(9559b25、bc76b0c 修正补充)动的是 opportunity_scheme_card.opp_id(方案卡「历史所属商机ID」遗留列,ADR-0041 方案卡 owner 化解耦后实体不再映射)。语义:
    • 迁移器 SchemeCardOwnerMigrationRunner(CommandLineRunner)启动时执行 ALTER TABLE opportunity_scheme_card MODIFY COLUMN opp_id bigint NULL COMMENT '(已废弃·owner 化解耦遗留)…'
    • bc76b0c仅在 IS_NULLABLE=NO 时执行(查 DatabaseMetaData,幂等——共享库每次启动不再重复 ALTER);
    • 动机:实体不映射该列,NOT NULL 会让 A3 新建方案卡首次 INSERT 即失败;旧行值保留供审计,新行恒 NULL。
  3. 结论:Demo 不做「新增/编辑客户挂商机」表单;演示关联动作落商机侧(§4 两条入口)。

3. 客户归属变更 × 商机级联矩阵

动作 端点 商机守卫 商机数据级联 Demo 可演示现象
领取 claim POST /api/customer/claim?id=(批量 -batch guardNoActiveOpportunity → 67007 有进行中商机时领取被 67007 拒(「客户存在进行中商机,不可抛入公海」文案族,动作=领取)
分配/换负责人 assign POST /api/customer/assign?id=&userId=(批量 -batch 无(不对称点) 只改客户 owner 四列快照 + 客户 oplog TRANSFER;名下商机 owner 原地不动 → 客户负责人与商机负责人可不一致
抛公海 releasePool POST /api/customer/release-pool?id=(批量 -batch :67007 无(放行时仅清客户 owner+协同人) 有进行中商机 → 67007 弹错;商机全部终结(4/5)→ 放行,存量终结商机不动
归档 archive POST /api/customer/archive?id= / -batch(整批预检) :67006(另:仅负责人可归档 67004) 同上;归档后 archiveStatus=2,从此商机详情候选池排除该客户(§4)
恢复 restore POST /api/customer/restore?id= 仅负责人 + 已归档才可(67004)
交割发起 initiate POST /api/customer/transfer/initiate(body 表单 reason[,toDirectorId]) 无守卫(不需要) 强级联·整批原子 逐客户 changeOwner→接收总监 + 商机 owner 同步 + 项目 owner 同步;任一失败全部回滚、不生成交接单;防重入 67010
交割分配 assignOne POST /api/customer/transfer/assign?id=&customerIds=&assignUserId= 强级联·逐条独立事务 被分配客户的商机 owner 三列刷成新销售;部分成功列原因;全部分配完 D23 自动置交接单已完成

3.1 商机 owner 同步实现(OpportunityOwnerSyncPortImpl

  • port:crm-customer/.../port/OpportunityOwnerSyncPortsyncOwnerByCustomer / syncOwnerByCustomers),实现在 crm-opportunity(@Primary 接管 Noop 兜底)。消费方仅交割两段(CustomerTransferServiceImpl L173/L242)——claim/assign/releasePool/archive 均不调用
  • 行为:经 opportunity_customer(delete_key=0)反查 oppIds → update(null, wrapper)owner_user_id / owner_name_snapshot / owner_dept_id 三列;接收人不存在抛业务异常回滚调用方事务;不 bump @Version(派生同步不与商机并发编辑 CAS 互扰);owner_dept_id 是商机 DataScope 锚点——「移交跟人走」口径。
  • 留痕不对称:项目侧 ProjectOwnerSyncPortImpl 同步外还逐项目写 ACTION_TRANSFER 动态(近期提交 c9d6e75,评审 P3-6);商机侧同步不写商机 oplog。Demo 若要留痕证据,看客户 oplog(TRANSFER 系统文案带交接单号 JG…),别等商机动态。

3.2 Demo 级联闭环建议(一条链演示全部现象)

建商机(带 customerId)→ 客户详情「关联商机」页签出现该商机(阶段=首节点展示名)→ 抛公海被 67007 阻断 → 商机置已关闭(4) 后抛公海放行 → 交割发起+分配 → 商机 owner/部门快照跟随变化 + 客户 oplog TRANSFER 留痕。

4. 反向:商机创建对客户的依赖

  • 弱依赖(customerId 可空)POST /api/opportunityOpportunityCreateController,tag A3 商机管理/新建商机)。硬必填仅 6 项:商机名称/商机来源/招标形式/省/市/项目属地(D-14)。customerId 非空时联动必填:isPrimaryIntended(仅 0/1)+ customerRole(66xxx 参数族)。
  • 两条建关联行的路径校验强度不同
    • 直接新建:OpportunityIntakeImpl.linkPrimaryCustomer 不做客户存在性/归档校验(customerNameSnapshot 原样取前端传值落库;isPrimaryIntended 缺省按 1、customerRole 缺省回退主要意向客户角色);
    • 商机详情挂客户:POST /api/opportunity/customer/addOpportunityCustomerServiceImpl.addCustomer,表单 oppId/customerId/customerRole/isPrimaryIntended)走 CustomerCatalogPort.requireLive 硬校验——不存在/已归档客户拒关联;重复关联友好化报错;设主要自动降旧主要(同事务)。
  • 归档客户与存量关联:归档不清理 opportunity_customer 存活行 → 客户归档后其「关联商机」页签仍能反查出商机;但商机详情侧不可再新挂该客户、候选池搜索也不出。
  • 客户侧无「从客户建商机」端点:crm-customer 的 ports 全是 query/sync/catalog 方向。API-SUMMARY §2.4「商机弹窗快建」是反向(商机表单里快建客户:POST /api/customer/quick-create + POST /api/customer/search)。
  • Demo「从客户发起建商机」:无专用端点也不需要新后端——用 POST /api/opportunity 预填 customerId(customerName 一并带出作快照)即可;落库后页签立即可见。建议提供该入口:一条链路同时演示双向关联 + 后续级联。

5. 端点 × 字段 × 触发场景速查

# 端点 关键入参 触发场景 对商机域的效果
1 GET /api/customer/opportunity/page?id= id, current, size 客户详情页签 4 只读反查 7 字段,零写副作用
2 POST /api/customer/claim?id= id 公海领取 有进行中商机 → 67007 阻断
3 POST /api/customer/assign?id=&userId= id, userId 换负责人 不级联商机(不对称点)
4 POST /api/customer/release-pool?id= id 抛公海 有进行中商机 → 67007 阻断
5 POST /api/customer/archive?id=-batch 整批预检) id / ids 归档 有进行中商机 → 67006 阻断
6 POST /api/customer/transfer/initiate reason[, toDirectorId] 交割第一段 整批原子:客户+商机+项目 owner 同刷接收总监
7 POST /api/customer/transfer/assign?id=&customerIds=&assignUserId= id, customerIds, assignUserId 交割第二段 逐条独立事务:商机 owner 跟随新销售;D23 自动完成
8 POST /api/opportunity customerId?, customerRole?, isPrimaryIntended?(customerId 非空时后两者必填) 从客户发起建商机(demo 合成入口) 建 opportunity_customer 行 → 页签可见;无 requireLive 校验
9 POST /api/opportunity/customer/add oppId, customerId, customerRole, isPrimaryIntended 商机详情挂客户 requireLive 硬校验(归档拒)+ 去重 + 设主降旧主

关联错误码:67004 状态不允许 / 67006 归档阻断 / 67007 抛公海阻断(claim 亦复用)/ 67010 交割防重入 / 67011 交割前置不合法 / 67012 分配对象非法 / 67002 客户不存在或已删除。