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