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.
40 lines
4.5 KiB
40 lines
4.5 KiB
|
16 hours ago
|
---
|
||
|
|
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_id` 换 `owner_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` 与旧索引 `idx_osc_opp` 保留为只读死列(共享库不做破坏性 DROP,应用代码零引用)。
|
||
|
|
3. **商机侧行为不变**:`OpportunitySchemeCardService` 商机入口签名不动(`listCards(oppId)` 等内部过滤 `owner_type=OPPORTUNITY`);商机卡保存的客户校验(必属本商机关联客户,无孤立卡)原样保留;商机列表/详情的方案卡列 join 加 `ownerType=OPPORTUNITY` 过滤(`OpportunityViewQueryImpl.fillSchemeCardColumns`、`OpportunityDetailServiceImpl` 兜底查)。
|
||
|
|
4. **PROJECT 归属校验接口化**:新增出站端口 `SchemeCardProjectPort`(crm-opportunity 声明 + Noop 桩,真实现住 crm-project `@Primary` 覆盖,对称 `OpportunityOwnerSyncPortImpl`/Noop 范式)。桩 **fail-closed**:`requireLiveProject` 抛 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 留痕」要求的留痕之一。
|