扫描范围:昨晚第三轮两候选(CustomerCatalogPort / OpportunitySubService 按 Tab 拆分)的落地验讫 + 最近两周最热区深扫—— crm-customer 返工票 09-13 收尾与缺陷修复链、客户交割(票 07)、归属流转(票 04)、导入执行器、商机拆分后的缝合处。 本轮沿用 codebase-design 词汇:module / interface / depth / seam / adapter / leverage / locality / deletion test。
port/CustomerCatalogPort(3 方法 + 随包 record + CustomerCatalogException)与 CustomerCatalogPortImplIntegrationTest(含「裸宽服务返全部、走 port 只返有效」对照断言)均已就位; 商机侧 OpportunityCustomerServiceImpl 只注入 port。落地形状与 ADR 决策一致,归档口径由端口钉死。
sub/ 七子包(客户/跟进/勘察/附件/团队/日志/工作计划)各持小 interface + 自家 impl; 聚合接口已删,OpportunitySubController 306 行薄适配层注入七服务,端点未动。拆分干净、无跨子域泄漏。
客户域的「换主」是一次协议而非一次赋值:owner 四列快照(userId/nameSnapshot/deptId/deptNameSnapshot)+ 部门名查表 + 在职守卫(enabled + employmentStatus=active)+ D25 锚点规则(last_valid_follow_time 空则赋当下、 enter_pool_time 清空)+ CAS 空列清空(MyBatis-Plus NOT_NULL 策略需 wrapper set null)+ 编号续号。 这串协议在 5 个文件手搓了 5 份(新建 / 领取·分配 / 交割发起 / 交割分配 / 导入建档),且漂移已经发生:
CustomerServiceImpl 版 catch NumberFormatException → 脏序号重起 1; CustomerImportExecutor 版不 catch(脏数据直接炸行)且封顶 9999。同一编号规则,两种边界行为。
create 恒 now;claim/assign「空则 now」;initiate「空则 now」; applyAssigneeSnapshot 不设锚点——依赖「initiate 必先设过」的时间链不变量,仅存在于注释里。
新增换主点忘设锚点 → CustomerReminderJob 的 isNotNull(last_valid_follow_time) 扫描过滤静默剔除该客户。不报错、不留日志——客户只是永远不被提醒。
同一「在职守卫」还长出 4 个错误码族(CODE_CUST_INVALID / CODE_ASSIGN_INVALID / CODE_TRANSFER_FORBIDDEN / CODE_MEMBER_INVALID), 5 份部门名查表 deptService.getDeptNames(Set.of(...)).get(...)。 Deletion test:若删掉一个假想的归属流转 module,快照+锚点+守卫+编号协议必须重新各找住处——复杂度收敛而非转移,测试成立。
crm-customer 立「归属流转」深 module(对称线索域既有形状 crm-lead/owner/OwnerSnapshotResolver, 2026-08-20 审查轮落地):换主动作(快照 + 锚点 + 守卫 + CAS 空列收一处)、编号续号收一处,五个调用点各缩成一行; D25 锚点语义从 javadoc 注释升格为 interface 契约。预设形态是 grilling 议题(守卫错误码归一 vs 保留语义分族、编号是否并入同 module、 导入执行器的影子建档路径接法),本报告不预设 interface。
锚点 + 快照 + 守卫协议住一个文件;「已归档/已离职还能不能换主」这类口径从隐式变一行契约(同 ADR-0033 requireLive 的赢法)。
A5 项目模块的 owner 同步、客户合并、未来批量换主——每个新换主点从「复刻 30 行协议」变一行调用。seam 每早立一天,协议就少复制一份。
锚点契约一份 module 级测试,替代今天散在 4 个流程测试里的 per-flow 断言(WorkspaceOwnership L400/425、Transfer L282、Import L722);「忘设锚点」从静默变红。
graph TD
subgraph 手搓协议×5
A1[CustomerServiceImpl
create→applyOwner]
A2[OwnershipServiceImpl
claim/assign→applyOwnerSnapshot]
A3[TransferServiceImpl
initiate 内联循环]
A4[TransferServiceImpl
applyAssigneeSnapshot
锚点靠时间链不变量]
A5[ImportExecutor
createCustomer 内联]
end
A1 & A2 & A3 & A4 & A5 -->|各写各的| C[(customer 表
owner 4列 + 锚点)]
C -->|isNotNull 过滤| J[CustomerReminderJob
无锚点=静默不提醒]
style A4 fill:#7f1d1d
style J fill:#7f1d1d
graph TD A1[create] --> M A2[claim/assign] --> M A3[交割两段] --> M A5[导入建档] --> M M[归属流转 module
接口 2-3 方法
快照+锚点+守卫+CAS+编号
全收实现] M -->|一行调用| C[(customer 表)] M -.->|契约钉死锚点| J[ReminderJob] T[module 契约测试
锚点/快照/守卫] -.-> M style M fill:#065f46 style T fill:#064e3b
判定语义已正确单点化(analyzer 深模块成立),但机制四周边缘仍是约定接缝:workbook 解析两处逐字复制(sheet0 必读 + sheet1 缺席容错,改一处另一处必漂);verdict 计数两份 switch(upload 的 insert/update/fail/suspect vs executor 再加 unchanged); 「他客户电话软提示」靠 reason 字符串前缀跨方法传协议(startsWith("疑似重复")——改文案即断功能)。 模板演进(加列、加 sheet)须同时改对四处。昨晚已报,今晨验讫:原样未动。
抽 CustomerImportReader(bytes → 两 sheet 行 + 模板表头校验单点),upload 与 executor 共用; ContactPlan 加显式 softSuspect 结构化契约替代字符串前缀;计数 tally 单点。收敛面克制:不收判定语义(analyzer 已对)、 不收行落库(executor 的真差异)。Benefits:预览与执行「同口径」从 javadoc 变同一份代码;模板演进只改一处; CustomerImportIntegrationTest(40KB)的守恒断言获得单一被测面。 与 #1 的交汇:executor 的 createCustomer 是影子建档路径(手搓 owner 快照 + 锚点 + 编号)——先落 #1 再收此处,导入建档接归属流转 module 即可。
graph TD U[upload] --> P1[EasyExcel 解析
副本1] E[executor] --> P2[EasyExcel 解析
副本2 逐字复制] U --> T1[计数 switch 1] E --> T2[计数 switch 2] AN[Analyzer 判定
单点 ✓] -.->|reason 字符串前缀
疑似重复…| E style P2 fill:#7f1d1d style T2 fill:#7f1d1d
graph TD U[upload] --> R[CustomerImportReader
bytes→两sheet行+表头校验] E[executor] --> R U --> T[tally 单点] E --> T AN[Analyzer 判定单点] -->|softSuspect 布尔
结构化契约| E style R fill:#065f46 style T fill:#064e3b
理由:(a) 同形修法在本仓已赢两次——线索 OwnerSnapshotResolver(2026-08-20 三件套之一)与客户 CustomerPendingNoticeWriter, 都是「散落写路径协议收一处」的照抄棋;(b) 漂移已发生——两份 nextCustomerNo 对脏序号行为分叉、锚点规则 4 形态, 这不是理论风险而是现存分叉;(c) 失败模式静默——忘设锚点的客户被提醒 Job 无声剔除,比报错难抓一个量级; (d) 消费方还在长——A5 项目模块的 owner 同步、客户合并都在路上,seam 每早立一天协议就少复制一份; (e) 顺序红利——与 #2 在导入执行器交汇,先立 #1,#2 收影子建档路径时直接接 seam,两票变成接力而非交叉。
下一步:选定后进入 /grilling 走决策树(interface 形状 / 守卫错误码归一 / 编号归属 / 导入接法 / 测试面存亡),决策落 ADR + CONTEXT.md 术语同步。