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

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.mdcrm-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 ≠ 源 idcode 不变(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):
    -- 预检: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)。