|
|
|
|
# 01 · CustomerOwnershipWriter:客户归属写入器(收五处手搓换主协议 + enter_pool_time 修复)
|
|
|
|
|
|
|
|
|
|
- **Status:** resolved
|
|
|
|
|
- **Type:** task
|
|
|
|
|
|
|
|
|
|
来源:`/improve-codebase-architecture` 第四轮(2026-09-07,候选 #1)+ grilling 八项拍板。决策全文见 **ADR-0034**(`docs/adr/0034-customer-ownership-writer.md`);术语见 `crm-customer/CONTEXT.md`「归属写入器」。本票单票一体(组件 + 五接线 + bug 修复 + 测试 + 文档),实现另起会话。
|
|
|
|
|
|
|
|
|
|
## 目标
|
|
|
|
|
|
|
|
|
|
customer 表 owner 列族的换主协议(快照四列 + D25 锚点 + 在职守卫 + CAS 空列清空)从 5 份手搓收成单一事实源;顺手修复 assign / 交割两段 `enter_pool_time` 未清空的现存契约违约。
|
|
|
|
|
|
|
|
|
|
## A. 新组件 ×2(`com.crm.customer.service`,具体 @Component 零接口,Writer 家族同构)
|
|
|
|
|
|
|
|
|
|
1. **`CustomerOwnershipWriter`** 三动词,mutate + 落库一体(事务边界留调用方外层 @Transactional):
|
|
|
|
|
- `initOwnerOnCreate(Customer entity, Long ownerUserId)`:mutate-only(insert 留调用方)。ownerUserId 空 = 公海分支(清四列快照 + enterPoolTime=now + 锚点 NULL);非空 = 守卫 + 快照 + 锚点=now。
|
|
|
|
|
- `changeOwner(Customer c, Long targetUserId)`:在职守卫(67012)→ 同主拒绝(67012)→ 快照四列 → 锚点空则 now、非空保留(统一规则,五场景无损)→ enterPoolTime **wrapper 显式清空** → CAS 落库(0 行 → 67005)。守卫依赖 IAuthUserService / ISysDeptService,落库依赖 CustomerMapper。
|
|
|
|
|
- `releaseToPool(Customer c)`:清 owner 两列(wrapper)+ enterPoolTime=now + 锚点保留 + CAS。协同人清空(teamMemberMapper.releaseAllByCustomerId)与进行中商机守卫**留调用方**。
|
|
|
|
|
2. **`CustomerNoGenerator`** `nextCustomerNo()`:KH+yyyyMMdd+4 位序(当日 MAX 续号),合并两版防御分支——脏序号 catch NumberFormatException 重起 1 + 封顶 9999。
|
|
|
|
|
|
|
|
|
|
守卫错误码统一直抛 67012(消息「销售负责人不存在、已停用或已离职」/「不能分配给当前负责人」),对称 CustomerDedupService 域内直抛;不用专用异常翻译(那是跨域 seam 口径)。
|
|
|
|
|
|
|
|
|
|
## B. 五调用点接线
|
|
|
|
|
|
|
|
|
|
| 调用点 | 改法 |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| `CustomerServiceImpl.create` | applyOwner → `initOwnerOnCreate`;nextCustomerNo → generator(构造器加 2 参) |
|
|
|
|
|
| `CustomerOwnershipServiceImpl.claim` | applyOwnerSnapshot + casUpdate → `changeOwner(c, currentUserId())`;claim 的 enterPoolTime wrapper 清空随之并入(现状行为不变) |
|
|
|
|
|
| `CustomerOwnershipServiceImpl.assign` | 同上 `changeOwner(c, targetUserId)`——**bug 修复点:enter_pool_time 从未清空(实体置 null + 平式 updateById 被 MP NOT_NULL 策略跳过),此处起真正清空** |
|
|
|
|
|
| `CustomerTransferServiceImpl.initiate` / `assignOne` | 内联换主循环 / applyAssigneeSnapshot + casUpdate → `changeOwner`(两处都修复 enter_pool_time) |
|
|
|
|
|
| `CustomerImportExecutor.createCustomer` | 手搓快照 + 锚点 + nextCustomerNo → `initOwnerOnCreate` + generator(离职 submitter 该行 fail——行为变化,见 D) |
|
|
|
|
|
|
|
|
|
|
删各服务的 applyOwner / applyOwnerSnapshot / applyAssigneeSnapshot / validateTargetUser / casUpdate / nextCustomerNo 私有方法(validateTargetUser 若仍被批量前置校验引用则保留一处)。
|
|
|
|
|
|
|
|
|
|
## C. 测试(双层)
|
|
|
|
|
|
|
|
|
|
1. 新增 `CustomerOwnershipWriterIntegrationTest`(H2,CustomerH2Harness 自举):设主锚点空则 now / 非空保留、**enterPoolTime 清空含 assign 场景(bug 防回归)**、公海分支、同主拒绝 67012、离职守卫 67012、CAS 冲突 67005、NoGenerator 脏序号重起 1 + 封顶 9999。
|
|
|
|
|
2. 既有 4 个流程集成测试(WorkspaceOwnership / Transfer / Import / ServiceImpl)原样保留作端到端锚;补 assign/交割后 enterPoolTime 清空断言(现状零断言即漏网原因)。
|
|
|
|
|
|
|
|
|
|
## D. 行为变化清单(ADR 已记,bruno 同步)
|
|
|
|
|
|
|
|
|
|
1. assign / initiate / assignOne 换主后 enter_pool_time 真正清空(修复违约,实体 javadoc 契约恢复)。
|
|
|
|
|
2. create / quickCreate 端点 owner 守卫失败 67001 → **67012**(bruno 文档同步)。
|
|
|
|
|
3. 交割发起人 = 部门 leader 的自分配被同主拒绝(原先静默换主给自己)。
|
|
|
|
|
4. 导入建档离职 submitter:原先不设防静默写快照 → 该行 fail(executor 逐行软处置兜住)。
|
|
|
|
|
|
|
|
|
|
## 验收
|
|
|
|
|
|
|
|
|
|
- `mvn test -pl crm-customer -am` 全绿;BOM 扫描 clean(AGENTS.md 编码条款);
|
|
|
|
|
- 全模块 grep 手动 `new CustomerServiceImpl|CustomerOwnershipServiceImpl|CustomerTransferServiceImpl|CustomerImportExecutor|CustomerWorkspaceServiceImpl` 的测试构造点并同步加参(历史坑:加依赖后手动 new 的测试编译红);
|
|
|
|
|
- ADR-0034 / CONTEXT.md 词条已落(本票前序已完成);bruno 的 create/quickCreate 错误码示例同步 67012。
|
|
|
|
|
|
|
|
|
|
## Answer
|
|
|
|
|
|
|
|
|
|
**2026-09-07 落地(实现会话)**:组件 ×2 + 五接线 + 双层测试 + bruno 同步全部完成;`mvn test -pl crm-customer -am` 全绿(crm-customer **186 tests / 0 failures**,含依赖 7 模块全 SUCCESS),BOM 扫描 clean。
|
|
|
|
|
|
|
|
|
|
- **组件**:`CustomerOwnershipWriter`(129 行)/ `CustomerNoGenerator`(40 行)如票 A 节;Writer 不挂 @Transactional(事务留调用方)。实现期修正一处:`BaseMapper.update` 返回 **int 非 boolean**,CAS 判 0 行用 `== 0`(`!int` 编译不过)。
|
|
|
|
|
- **五接线**:create / claim / assign / initiate + assignOne / 导入建档全部改走 Writer + generator;claim 的公海守卫、商机守卫与 oplog 留各调用方,现状行为不变。
|
|
|
|
|
- **实施差异**(票面未细化处,如实记):
|
|
|
|
|
- `validateTargetUser` 未删而 **void 化保留**——assignBatch 批量前置统一校验仍引用(B 节末行预留情形);单条守卫已收编 Writer。
|
|
|
|
|
- `casUpdate` 收敛为 `casUpdateNoClear`(`this.updateById` + 0 行→67005)留 OwnershipServiceImpl,仅 archive/restore 用。
|
|
|
|
|
- 交割 initiate 的 CAS 失败消息统一为 Writer 文案「客户已被他人变更,请刷新后重试」(原「客户【name】已被他人变更」;测试无消息断言,安全)。
|
|
|
|
|
- 导入建档离职 submitter:67012 被 executeImport 行级 catch 软处置兜住 → 该行 fail「执行异常:销售负责人不存在、已停用或已离职」(D4 预期形态)。
|
|
|
|
|
- **测试**:`CustomerOwnershipWriterIntegrationTest` 14 用例全绿(锚点空则 now / 非空保留、**assign 场景 enterPoolTime 清空防回归**、建档两分支、同主/停用/缺失 67012、CAS 冲突 67005、抛公海保留锚点、NoGenerator 续号/脏序号重起/封顶 9999);4 个流程集成测试补 6 处 `getEnterPoolTime()).isNull()` 断言(WorkspaceOwnership claim+assign、Transfer initiate+assignOne——两处均先注入存量脏值模拟、Import E2E 建档、ServiceImpl create 建档)。
|
|
|
|
|
- **bruno**:`新增客户.bru` / `快速创建客户.bru` 错误码清单各加 `- 67012 销售负责人不存在、已停用或已离职`(分配客户 / 批量分配原有 67012 语义一致不动)。
|
|
|
|
|
- **测试构造点**:全模块 grep 五服务手动 new 共 9 处(6 文件)——7 处改参同步(WorkspaceOwnership ×2、Transfer、ServiceImpl、Contact、CatalogPort、Import),CustomerWorkspaceServiceImpl ×2 签名未变无需动。
|
|
|
|
|
|
|
|
|
|
## Comments
|
|
|
|
|
|
|
|
|
|
- grilling 拍板记录(八问全选推荐项):只收协议机制 / mutate+落库一体 / 三动词 / 统一 67012 / NoGenerator 独立 / CustomerOwnershipWriter 定名 / 双层测试 / 单票一体。
|
|
|
|
|
- 锚点统一规则推演:create 恒 now ⇔ 实体锚点 null 时「空则 now」;assignOne 不设 ⇔ 锚点必非空时「非空保留」——五场景零行为变化(除 D 清单)。
|