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
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}[¤t=&size=](CustomerDetailController.pageOpportunities,tagA4 客户管理/客户详情/关联商机;BaseParam 分页) - 链路:
CustomerDetailServiceImpl.pageOpportunities(仅 require 客户存在 67002) → 出站 portcrm-customer/.../port/CustomerOpportunityQueryPort.pageByCustomerId→ crm-opportunityport/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-ruledescribeStages批量解析(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.countActiveByCustomer:
opp_status IN (1,2,3)(1 待领取 / 2 推进中 / 3 暂缓中)计数 > 0。已关闭 4 / 已转项目 5 视为终结,不阻断。
2. 「新增/编辑客户挂商机」——票面前提修正
- customer 侧没有「挂商机」字段与端点:
Customer实体、CustomerCreateDTO、CustomerUpdateDTOgrepopp|opportunity均 0 命中;POST /api/customer/create/POST /api/customer/edit?id=字段面与商机无关(edit 且「归属字段不可改」,API-SUMMARY §2.3)。客户↔商机关联唯一写入口在商机侧(opportunity_customer子表)。 - 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。
- 迁移器
- 结论: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/OpportunityOwnerSyncPort(syncOwnerByCustomer/syncOwnerByCustomers),实现在 crm-opportunity(@Primary接管 Noop 兜底)。消费方仅交割两段(CustomerTransferServiceImplL173/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/opportunity(OpportunityCreateController,tagA3 商机管理/新建商机)。硬必填仅 6 项:商机名称/商机来源/招标形式/省/市/项目属地(D-14)。customerId非空时联动必填:isPrimaryIntended(仅 0/1)+customerRole(66xxx 参数族)。 - 两条建关联行的路径校验强度不同:
- 直接新建:
OpportunityIntakeImpl.linkPrimaryCustomer不做客户存在性/归档校验(customerNameSnapshot 原样取前端传值落库;isPrimaryIntended 缺省按 1、customerRole 缺省回退主要意向客户角色); - 商机详情挂客户:
POST /api/opportunity/customer/add(OpportunityCustomerServiceImpl.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 客户不存在或已删除。