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.
8.7 KiB
8.7 KiB
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-ruleOpportunityStageTemplateServiceImplL250 附近OpportunitySchemeCardTemplateServiceImplL193 附近OpportunityPoolRuleServiceImplL102 附近
- 修法:编辑分支生成新草稿时清空 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)所属部门快照写入。 - 流转路径:
OpportunityTransitionImplL120/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.pyPart A,ADMIN 会话):三族「publish→编辑发布中」全绿——新草稿id ≠ 源 id、code不变(POOL_RULE_20 / OPP_STAGE_TPL_09 / OPP_SCHEME_TPL_12)、versionNoV1.0→V1.1 顺延、status=1草稿,可再发布自我顶替。旧版实测的 50001 不再复现。
B. D-13 owner_dept_id 恒空
- 根因四处:① 创建路径
OpportunityCreateServiceImplspec 硬编码 null;② 线索转入OpportunityCreationPortImpl直接透传cmd.ownerDeptId()(线索快照缺失时恒空);③ 流转五端点(claim/claim-batch/assign/assign-batch/handover)ownerNameSnapshot/ownerDeptIdrequired=false直取前端、不校验不兜底。 - 修法(ADR-0017 选「服务端覆盖」):新建 owner 快照组件三件套
crm-opportunity/owner/(OwnerSnapshotrecord /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):-- 预检: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.pyPart 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)。