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.
 
 
 
 
 

4.5 KiB

status
accepted

方案卡 owner 化解耦:opp_id → owner_type + owner_id 双归属(prefactor,动 crm-opportunity)

Context

A5 项目管理(crm-project,独立建表 crm_project,见 deliverable-spec §2.1 路线甲)要求「项目关联方案卡必填」,且方案卡要能归属项目。但方案卡表 opportunity_scheme_card 现为商机专属opp_id bigint not null,唯一约束 UNIQUE(opp_id, customer_id, delete_key)。商机侧 opp_status=5(已转项目) 降级为纯终态标记后,项目不再寄居商机表——方案卡若仍绑死 opp_id,项目卡无处安放。

这是 crm-project 全部 19 票的地基票(01):不动它,POST /project/create 的方案卡必填校验无从谈起。

约束:

  • 商机侧既有行为(客户校验、唯一约束、列表 join、快照回写)回归不变——存量方案卡全部是商机卡;
  • 存量数据自动迁移(行随 ownerType=OPPORTUNITY, ownerId=旧oppId 归位);
  • PROJECT 归属的客户校验语义 = 卡 customer_id 必等于项目主档 crm_project.customer_id(一项目一客户一卡,spec §7 拍板),但 crm-opportunity 不得反向依赖 crm-project。

Considered Options

  • A(选中,spec §7 拍板):就地改列——opportunity_scheme_card 表名不动、模块不搬,opp_idowner_type + owner_id,唯一约束换四列。爆炸半径最小:service + view query 两个 owner 类 + 两个读侧过滤点。
  • B:搬模块/改表名(如迁去 crm-project 改名 scheme_card)。数据改动与 A 相同,仅多搬模块、改表名纯语义成本,爆炸半径翻倍、动商机核心聚合,否(spec §7 已否)。
  • C:留 opp_id 加可空 project_id 列。两列互斥语义靠约定维持,唯一约束要拆成两条且无法表达「归属轴」,后续每处 join 都要 COALESCE,否。

Decision

  1. 归属列oppId(not null) → ownerType(tinyint: 1=OPPORTUNITY/2=PROJECT, default 1) + ownerId(bigint not null)。唯一约束 uk_osc_opp_customer(opp_id, customer_id, delete_key)uk_osc_owner_customer(owner_type, owner_id, customer_id, delete_key)opp_id 命名陷阱检查通过(AGENTS.md,无需钉列名)。
  2. 存量迁移:启动期 SchemeCardOwnerMigrationRunner(幂等,按列/索引存在性判断)——老库补列、存量行回填 owner_type=1, owner_id=opp_id、唯一约束换轨;旧列 opp_id 放宽为可空死列(实体已不映射,新插卡不带该列,NOT NULL 会让 A3 新建卡 INSERT 失败——真实环境实测修正;旧行值保留供审计)。
  3. 商机侧行为不变OpportunitySchemeCardService 商机入口签名不动(listCards(oppId) 等内部过滤 owner_type=OPPORTUNITY);商机卡保存的客户校验(必属本商机关联客户,无孤立卡)原样保留;商机列表/详情的方案卡列 join 加 ownerType=OPPORTUNITY 过滤(OpportunityViewQueryImpl.fillSchemeCardColumnsOpportunityDetailServiceImpl 兜底查)。
  4. PROJECT 归属校验接口化:新增出站端口 SchemeCardProjectPort(crm-opportunity 声明 + Noop 桩,真实现住 crm-project @Primary 覆盖,对称 OpportunityOwnerSyncPortImpl/Noop 范式)。桩 fail-closedrequireLiveProject 抛 66015 明确拒绝——「无孤立卡」铁律不允许校验缺位时静默放行。保存入口按 dto.oppId/dto.projectId 互斥分叉(双归属互斥,一次一张归属的卡)。
  5. 提交路径差异:项目卡无商机「暂缓」守卫、无商机主表快照回写(D-17 仅商机卡);提交必填校验等其余逻辑共用。

Consequences

  • 商机侧全部既有行为回归不变(271 既有测试绿 + 新增 PROJECT 通道 4 用例钉死桩语义/互斥/客户校验/落库形态)。
  • opportunity_scheme_card 物理表在老库迁移后残留只读死列 opp_id(含旧索引);新装库(H2/新环境)直达新形态。后续如做破坏性清理,须单独立票。
  • SchemeCardDTO 新增 projectId/ownerType 字段;商机侧 HTTP 契约不变(oppId 语义照旧),项目归属卡的落库通道本期仅服务级暴露,HTTP 端点由 A5(票 02)挂载。
  • crm-project(票 02)上线后:真实现覆盖桩 → 66015 消失,POST /project/create 的「方案卡必填 + 卡客户=项目客户」校验由该端口承担。
  • 决策来源:crm-project-wayfinding 票 07(方案卡解耦)+ grilling 补拍(spec §7/§8);本 ADR 即 spec §8「CONTEXT.md/ADR 留痕」要求的留痕之一。