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.

92 lines
8.7 KiB

1 week ago
# 03: P1 修复 A — V-CONFIG 三族 resolveDraft 主键冲突(D-10/11/12)+ owner_dept_id 恒空(D-13)
`Type: task`
`Status: resolved`
`Blocked by: 02`
## 任务
修两个结构性 P1。改动前读 `crm-rule/CONTEXT.md`、`crm-opportunity/CONTEXT.md`。
### A. D-10/11/12:三族「编辑发布中版本 → 生成新草稿」报 50001 主键冲突
- 现象:对发布中版本点编辑,系统应「以发布版为基线生成新草稿」,实际 `dto.toEntity()` 携带源记录 id 走 insert → 主键冲突,**V-CONFIG 版本推进 API 全断**。
- 根因锚点(三族同构,三处一起修):
- `crm-rule` `OpportunityStageTemplateServiceImpl` L250 附近
- `OpportunitySchemeCardTemplateServiceImpl` L193 附近
- `OpportunityPoolRuleServiceImpl` L102 附近
- 修法:编辑分支生成新草稿时清空 id(以及 version/code 等由服务端生成的键),保证 insert 走新主键;参考同族 `copy` 端点的独立编码实现(票 08 上轮实测 copy 是好的)。
- 测试:三族各写/改一个单测覆盖「发布中 → 编辑 → 新草稿落库(id 不同、ruleCode/templateId 独立、状态=草稿)」;跑 `e2e-rules.py` 对应段回归。
### B. D-13:owner_dept_id 全表恒空
- 现象:13/13 条商机 owner_dept_id NULL → 部门专用公海规则永不命中 + DataScope 部门天花板失效。
- 根因锚点:
- 创建路径:`OpportunityCreateServiceImpl` 注释「ownerDeptId 传 null:直接新建不带部门快照」→ 改为取当前用户(token)所属部门快照写入。
- 流转路径:`OpportunityTransitionImpl` L120/L137 `.set(Opportunity::getOwnerDeptId, cmd.ownerDeptId())` 依赖前端传参 → 改为服务端以**新负责人**的部门为准回填(claim/assign/handover 都是换人语义,部门应跟人走)。
- 数据修复:对存量 NULL 行补一次回填(UPDATE 用负责人 user 的 dept;SQL 先 SELECT 确认影响行数再执行,远程库 8.129.84.155,谨慎)。
- 测试:创建/claim/assign/handover 四路径各验 owner_dept_id 落值正确 + 部门专用公海规则命中(U03 场景重测)。
## 约束
- 遵守 ADR-0017(Controller IO/Param DTO 约定):不因修 bug 破坏既有契约;ownerDeptId 若从请求 DTO 移除注意兼容(保留字段但忽略,还是直接服务端覆盖——按最小侵入选)。
- 修完跑 BOM 扫描(`.scratch/bom-scan.ps1` 的用法,AGENTS.md 编码纪律):新改文件 UTF-8 无 BOM。
## 验收
- `mvn compile`(或至少 crm-rule + crm-opportunity 模块)全绿。
- 三族「编辑发布中→新草稿」实测不再 50001,新草稿可编辑、可发布顶替。
- owner_dept_id:新建 1 条 + claim/assign/handover 各 1 次后字段非空且等于负责人部门;部门专用公海规则命中。
- 存量数据回填完成(SQL 记录进 Answer)。
## Answer
(20260829 完成)A/B 两部分全部落地,验收对照见文末。
### A. D-10/11/12 三族「编辑发布中→新草稿」50001
- **根因**:三族同构——`resolveDraft` 第三分支(发布中/停用→生新草稿)用 `dto.toEntity()` 直接 insert;`toEntity()` 经 `BeanUtils.copyProperties` 把 BaseDTO 的 `id`/`createTime`/`updateTime` 一并复制,携带源行主键 insert → 50001。第二分支(草稿原位编辑)走 `updateById` 无恙,故只在编辑发布中/停用时炸。
- **修法**(三处对称):第三分支 insert 前清三毒——`setId(null)` + `setCreateTime(null)` + `setUpdateTime(null)`(主键走雪花新键,审计时间戳清空走 MetaObjectHandler 自动填充)。
- `crm-rule/.../OpportunityStageTemplateServiceImpl.java`(D-10)
- `crm-rule/.../OpportunitySchemeCardTemplateServiceImpl.java`(D-11)
- `crm-rule/.../OpportunityPoolRuleServiceImpl.java`(D-12)
- **单测**:三族测试各补「insert 时刻主键为 null + 审计时间为 null」断言(mock insert stub 会回填 id,须在 stub 内记录 `insertedIdAtInsert`/`AtomicReference idAtInsert`,captor 直读会读到回填后值——50001 复现点断言必须取 insert 时刻)。结果 95/95 全绿。
- **顺带修复 4 个存量失败用例**(与本次改动无关):Stage 测试 `CODE_C1="OPP_STAGE_TPL_01"` 恰为 D-01 种子保护编码 `SEED_STAGE_TPL_CODE`,disable/delete 用例期望 64013/64015 实得 64018 保护拦截。修法:新增 `CODE_NON_SEED="OPP_STAGE_TPL_09"`,仅 4 个用例 fixture 换码(不动全局 CODE_C1,防连锁破坏 copy 用例的编码顺延断言)。
- **实测**(`t03-verify.py` Part A,ADMIN 会话):三族「publish→编辑发布中」全绿——新草稿 `id ≠ 源 id`、`code` 不变(POOL_RULE_20 / OPP_STAGE_TPL_09 / OPP_SCHEME_TPL_12)、`versionNo` V1.0→**V1.1** 顺延、`status=1` 草稿,可再发布自我顶替。旧版实测的 50001 不再复现。
### B. D-13 owner_dept_id 恒空
- **根因四处**:① 创建路径 `OpportunityCreateServiceImpl` spec 硬编码 null;② 线索转入 `OpportunityCreationPortImpl` 直接透传 `cmd.ownerDeptId()`(线索快照缺失时恒空);③ 流转五端点(claim/claim-batch/assign/assign-batch/handover)ownerNameSnapshot/ownerDeptId `required=false` 直取前端、不校验不兜底。
- **修法**(ADR-0017 选「服务端覆盖」):新建 owner 快照组件三件套 `crm-opportunity/owner/`(`OwnerSnapshot` record / `OwnerSnapshotResolver` 接口 / `OpportunityOwnerSnapshotResolverImpl`——类名带前缀因与 crm-lead 同名实现类默认 bean 名冲突,`ConflictingBeanDefinitionException` 实测修复);解析自 `IAuthUserService.getById`(对齐 crm-lead 同型组件:用户不存在返回 (userId,null,null) 不报错)。三个消费方改造:创建按创建人部门落快照;线索转入 cmd 缺失时兜底解析;流转五端点**删快照请求参数**改服务端 `of(operatorId/newOwnerUserId)` 解析(多传参数被 Spring 静默忽略,符合 ADR-0017 删签名安全)。语义:claim/assign=快照跟新负责人走,handover=部门完整过户,`origin_dept_id` 血缘不变。
- **数据回填**(远程库谨慎两段式,脚本 `.scratch/opportunity-bugfix/backfill-owner-dept.py`):
```sql
-- 预检:13 行(与 e2e-report D-13 的 13/13 吻合)
SELECT o.id, o.opp_status, o.owner_user_id, o.owner_dept_id, u.dept_id AS user_dept_id
FROM opportunity o JOIN crm_auth_user u ON u.id = o.owner_user_id
WHERE o.deleted=0 AND o.owner_dept_id IS NULL AND o.owner_user_id IS NOT NULL;
-- 执行:affected = 13
UPDATE opportunity o JOIN crm_auth_user u ON u.id = o.owner_user_id
SET o.owner_dept_id = u.dept_id
WHERE o.deleted=0 AND o.owner_dept_id IS NULL AND o.owner_user_id IS NOT NULL;
-- 复核:残留 0
SELECT COUNT(*) FROM opportunity WHERE owner_dept_id IS NULL AND owner_user_id IS NOT NULL AND deleted=0;
```
- **实测**(`t03-verify.py` Part B,DB 直查 + detail 双口径):B1 创建(A)→ owner_dept_id=744841308564094976(A 部门)✅;B2 抛池→assign→B → 744841308677341184(B 部门)✅;B3 handover→C → 744841292483133440(C 部门,完整过户)✅;B4 抛池→claim→B → 744841308677341184 ✅。四路径快照全部跟人走。
- **U03 部门专用规则**:配置层 ✅——部门专用规则(applyScope=2, deptIds=C 部门, allowManualPool=0)发布成功、deptIds 绑定回显正确、64012「一个部门只能被一条发布中规则覆盖」校验工作正常(重复发布被拒)。生效层 release-pool 仍放行 = **D-09 已立案既有缺陷(票 05 B 范围)**:`OpportunityPoolRuleMatcher.loadPublished()` 目前仅被超期回收 Job 消费,allowManualPool/allowFreeClaim 在 claim/release 入口 0 处接线(Grep 实证);release 按票 02 裁决口径默认不受规则约束(抛是主动行为)。票 03 修复的 owner_dept_id 锚点正是票 05 接线的前置条件,现已就绪。
### 验收对照
| 验收项 | 结果 |
|---|---|
| mvn compile(crm-rule + crm-opportunity -am) | ✅ 全绿 |
| 三族编辑发布中→新草稿不 50001,可发布顶替 | ✅ 实测 V1.1 顺延 |
| 四路径 owner_dept_id 非空且=负责人部门 | ✅ DB 直查四断言 |
| 部门专用公海规则命中 | ✅ 锚点+配置层;生效层拦截=D-09(票 05) |
| 存量回填 SQL 记录 | ✅ affected=13,残留 0 |
| BOM 扫描 | ✅ ALL CLEAN(bom-check2.log) |
### 工程记录
- 验证脚本 `t03-verify.py`(P 预检/B 四路径/A 三族/U 生效层/CLEANUP 五段,复用 e2e-rules.py Sess 封装);B 流程踩状态机语义:E2 assign/E1 claim 前置=待领取(1)(跟进中直接 assign 报 66003),验证流须先抛池。
- 服务重启踩坑:两域同名实现类 bean 冲突(见上);环境工具链封装 build.cmd/build-test.cmd/build-package.cmd/boot.cmd(mvn/java/python 均不在 PATH)。