Browse Source

ticket01修正: 迁移器放宽遗留列 opp_id 为可空(实体不再映射,NOT NULL 会让 A3 新建卡 INSERT 失败,真实环境实测)+ ADR-0041 修订

master
luoweijian 9 hours ago
parent
commit
9559b251d7
  1. 5
      crm-opportunity/src/main/java/com/crm/opportunity/config/SchemeCardOwnerMigrationRunner.java
  2. 2
      docs/adr/0041-scheme-card-owner-decouple.md

5
crm-opportunity/src/main/java/com/crm/opportunity/config/SchemeCardOwnerMigrationRunner.java

@ -67,6 +67,11 @@ public class SchemeCardOwnerMigrationRunner implements CommandLineRunner {
"UPDATE " + TABLE + " SET owner_type = 1, owner_id = opp_id" "UPDATE " + TABLE + " SET owner_type = 1, owner_id = opp_id"
+ " WHERE owner_id IS NULL OR owner_id = 0"); + " WHERE owner_id IS NULL OR owner_id = 0");
log.info("[scheme-card-owner-migration] 存量方案卡回填商机归属 {} 行", backfilled); log.info("[scheme-card-owner-migration] 存量方案卡回填商机归属 {} 行", backfilled);
// 放宽旧列为可空:实体已不映射 opp_id,新插卡不带该列——NOT NULL 会让 A3 新建卡
// 首次 INSERT 即失败(真实环境实测)。旧行值保留供审计,新行 NULL。
jdbcTemplate.execute("ALTER TABLE " + TABLE + " MODIFY COLUMN opp_id bigint NULL"
+ " COMMENT '(已废弃·owner 化解耦遗留)历史所属商机ID,新行恒 NULL'");
log.info("[scheme-card-owner-migration] 旧列 opp_id 已放宽为可空");
} }
if (!indexExists(conn, TABLE, "uk_osc_owner_customer")) { if (!indexExists(conn, TABLE, "uk_osc_owner_customer")) {

2
docs/adr/0041-scheme-card-owner-decouple.md

@ -25,7 +25,7 @@ A5 项目管理(crm-project,独立建表 `crm_project`,见 deliverable-spe
## Decision ## 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,无需钉列名)。 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,应用代码零引用)。 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.fillSchemeCardColumns`、`OpportunityDetailServiceImpl` 兜底查)。 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` 互斥分叉(双归属互斥,一次一张归属的卡)。 4. **PROJECT 归属校验接口化**:新增出站端口 `SchemeCardProjectPort`(crm-opportunity 声明 + Noop 桩,真实现住 crm-project `@Primary` 覆盖,对称 `OpportunityOwnerSyncPortImpl`/Noop 范式)。桩 **fail-closed**:`requireLiveProject` 抛 66015 明确拒绝——「无孤立卡」铁律不允许校验缺位时静默放行。保存入口按 `dto.oppId`/`dto.projectId` 互斥分叉(双归属互斥,一次一张归属的卡)。
5. **提交路径差异**:项目卡无商机「暂缓」守卫、无商机主表快照回写(D-17 仅商机卡);提交必填校验等其余逻辑共用。 5. **提交路径差异**:项目卡无商机「暂缓」守卫、无商机主表快照回写(D-17 仅商机卡);提交必填校验等其余逻辑共用。

Loading…
Cancel
Save