# 实现进度交接 — 客户模块 /implement 流水线(2026-09-03 · 第二版)
> 用途:交接给**全新会话**继续 `/implement .scratch\customer-module`。
> 与同目录 `REVIEW-HANDOFF.md`(spec 复核清单)**不同用途**——那份是 spec 阶段的 grill 交接,本份是**代码实现进度**交接。
> 权威决策仍在:`map.md`(D1–D29)+ `tickets/NN-*.md`(13 票验收项)+ `issues/NN-answer.md`(逐票定稿)。本份**不重复**其内容,只记进度、状态、陷阱。
---
## 1. 全局进度(13 票流水线)
| 票 | 状态 | 测试 | 备注 |
|---|---|---|---|
| 01 模块骨架 | ✅ | ping | — |
| 02 主表 CRUD + 三层查重 | ✅ | 17 | CustomerDedupService 三层(L1/L2/L3) |
| 03 联系人子表 | ✅ | 17 | CustomerContactServiceIntegrationTest |
| 04 三 workspace + 归属流转 | ✅ | 14 | CustomerWorkspaceOwnershipIntegrationTest |
| 05 客户阶段三态 CAS + port seam | ✅ | 7 | CustomerStagePortIntegrationTest |
| 06 详情 8 页签 + 跟进 + oplog | ✅ | 8 | CustomerFollowDetailIntegrationTest |
| 07 客户交割两段式 | ✅ | 12 | CustomerTransferIntegrationTest;crm-dict 种子 +1 组 |
| **08 客户导入** | ✅ **第二会话完成** | **15** | CustomerImportIntegrationTest;全模块 90/90 绿 |
| **09 crm-rule 客户规则族 + Job** | ✅ 第三会话收尾 | 回归 146+99 全绿 | 详见第 3 节 |
| **10 saved-view 接入** | ✅ 第三会话完成 | ** +22**(14 单测 + 8 集成) | scope_key 三值 + CustomerSavedViewFilter + savedViewId 落基类;见 3.5 节 |
| **11 商机侧切真源** | ✅ 第四会话完成 | ** +5**(2 新 Port 测试类 + SubService/customer 侧更新) | CustomerOpportunityQueryPort/OwnerSyncPort 真实现接管 Noop;StagePort 本期无调用方不动;见 3.6 节 |
| 12+13 收尾 | ⏳ | — | 全量测试 + 冒烟 + 文档核对 |
| 14 code-review + 提交 | ⏳ | — | 用户明确要求:先 code-review 再提交 |
**测试计数现状**:crm-customer **122 全绿** (121 + 票 11 新增 1)、crm-opportunity **242 全绿** (含票 11 新增 5 用例,第四会话三模块回归确认)。crm-dict DictDataInitializerTest 8 全绿(24 组 / 229 项)。crm-rule **146 全绿** (既有 + 票 09 新增 2 类)。
---
## 2. 票 08 收工记录(第二会话完成,90/90 全绿)
### 2.1 新增/修改文件
- `crm-customer/.../controller/CustomerImportController.java` (新建 93 行,6 端点:upload/confirm/result/failures/page/template;template 返回 `ResponseEntity<byte[]>` ,RFC 5987 中文文件名 `URLEncoder.encode(...).replace("+", "%20")` + `filename*=UTF-8''` )
- `crm-customer/.../domain/dto/ImportFailDTO.java` (新建 48 行)——**ADR-0017**:failures 端点返回纯展示 DTO(sheetName/rowNum/customerIdentity/failField/failReason + `fromEntity()` ),不返回 `CustomerImportFail` 实体(泄漏 BaseEntity 审计字段);对称 CustomerBatchFailItem
- `ICustomerImportService` / `CustomerImportServiceImpl` ——failures 签名改 `List<ImportFailDTO>` ;`TEMPLATE_NAME` 改 public(Controller 引用);补漏的 `CustomerImportTaskMapper` import
- `CustomerImportAnalyzer` ——ContactPlan 补 `ownerId()` ;`existsContactOnOtherCustomer` 的 `.ne` 改条件形式 `.ne(excludeCustomerId != null, ...)` (null 时恒假 SQL 坑)
- `CustomerImportExecutor` ——联系人 UPDATE 分支按 issues-09 §C 补齐 name/jobTitleName/isKeyContact 回写(原只回写 phone = 空更新,spec 偏差)
- **根 `pom.xml` **——properties + dependencyManagement 钉 **commons-io 2.16.1** (陷阱 ⑩,勿动)
- `crm-customer/src/test/.../CustomerImportIntegrationTest.java` (843 行 **15 测试全绿** ;文件已规范化为 **LF 行尾** ,可正常 SearchReplace)
### 2.2 测试口径备忘(实现已验证,后续改代码别踩)
- preview 的 `failCount` **只聚合客户 sheet** plans;联系人失败只进 `previewRows` 明细(测试断言 `isZero()` + `contains(tuple(SHEET_CONTACT, VERDICT_FAIL))` )——实现口径如此,测试已对齐
- EasyExcel **跳过全 null 空行** ——测试造数行至少一列非空
- H2 测试骨架抄 CustomerTransferIntegrationTest:5 张表 SCHEMA 全列(customer/customer_contact/customer_oplog/customer_import_task/customer_import_fail);SecurityUtils 静态 mock 需同时 stub `getUserId` (MetaObjectFillHandler 审计填充)与 `getRequiredUserId` (page 过滤);FileApi 桩 upload 存字节 / download 回放
---
## 3. 票 09 当前状态(✅ 第三会话已收尾:回归 146+99 全绿、BOM 扫描 clean、t09 COMPLETE)
### 3.1 已落盘且编译通过的 15 个文件
**crm-rule 侧(9 主 + 2 测试)**——客户规则族第一例,全局单例范式:
```
constant/CustomerRuleConstants.java FLAG_NO/YES、SINGLETON_ID=1、出厂默认、64023
domain/entity/CustomerReminderRule.java 单例行 id=1;不套 V-CONFIG 版本化(chart D4)
mapper/CustomerReminderRuleMapper.java BaseMapper 空接口
domain/dto/CustomerReminderRuleDTO.java GET 回显 + PUT 全量覆盖体(无审计字段)
service/ICustomerReminderRuleService.java current() / save()
service/impl/CustomerReminderRuleServiceImpl.java 全量覆盖;校验 64023(天数>0);开关缺省=0
config/CustomerReminderRuleInitializer.java @Order (18) 首装默认行(全开/30天/7天);幂等键=单例行存在
config/CustomerReminderRulePermissionInitializer.java @Order (14) 仅菜单挂「客户管理」目录;权限码 customer.reminder.rule;不种子 button
controller/CustomerReminderRuleController.java GET/PUT(RBAC customer.reminder.rule)
test/config/CustomerReminderRuleInitializerTest.java ✅ 全绿
test/service/impl/CustomerReminderRuleServiceImplTest.java ✅ 全绿
```
**crm-customer 侧(3 主 + 1 测试)**:
```
domain/entity/CustomerPendingNotice.java 待发通知(notice_status 0待发/1已发/2已失效);payload_json 存 customerId/anchor/remindSeq
mapper/CustomerPendingNoticeMapper.java
job/CustomerReminderJob.java @Scheduled 每日;锚点=customer.last_valid_follow_time(Q15=A);
失效=锚点漂移或客户归档;无配置/总开关关→整轮跳过;本期只落待发不发送
test/job/CustomerReminderJobIntegrationTest.java ✅ 9/9 绿
```
**CONTEXT.md**:`crm-rule/CONTEXT.md` 已补「客户规则子域(票 09)」段(错字已修);`crm-customer/CONTEXT.md` 超期提醒术语已提前存在。依赖方向:crm-customer 直接 import crm-rule 的 service/entity(与 crm-opportunity→crm-rule 同范式);`@EnableScheduling` 已在 crm-file SchedulingConfig 全局生效,Job 无需另配。
### 3.2 待办(✅ 第三会话全部完成:①回归 146+99 全绿 ②BOM 扫描 clean ③t09 置 COMPLETE;下列 3 条为历史记录)
1. **全模块回归** (上一会话最后一条命令被取消,未跑):
`$env:JAVA_HOME='D:\IntelliJ IDEA 2023.3.8\jbr'; & d:\code\crm-backend-matt\tmp\apache-maven-3.9.9\bin\mvn.cmd -q -s d:\code\crm-backend-matt\settings.xml test -pl crm-rule,crm-customer -o`
预期:crm-customer 99 全绿(90 + Job 9)、crm-rule 既有 + 2 新类全绿。结果以 surefire 报告统计为准(陷阱 ⑮)。
2. **BOM 扫描** (票 09 的 15 个文件写于上次全仓扫描之后):全仓扫描命令见第 5 节。
3. TodoWrite 置 t09 COMPLETE → 读 `tickets/10-*.md` + 对应 `issues/NN-answer.md` 推进票 10。
---
## 3.5 票 10 收工记录(第三会话完成)
### 3.5.1 新增/修改文件
**新增(3 主 + 2 测试)**:
- `query/CustomerSavedViewFilter.java` —— filter_json → 结构化条件翻译器。对称商机版但输出形态不同:客户侧查询走「Query 参数对象 + 注解 SQL」(非 wrapper),故翻译成 `CustomerViewCondition` 列表,由 `pageWorkspace` 的 XML `<choose>` 白名单落 SQL(`${sv.column}` 白名单常量拼接,值全 `#{}` 参数化);scope 约束内聚(owner 两字段仅 overview/mine、enterPoolTime 仅 pool,越 scope 静默跳过)
- `query/CustomerViewCondition.java` —— 翻译输出载体(column/op/value/values)。**用 `@Data` class 不用 record**:MyBatis OGNL 属性访问按 getter 解析,record 无前缀 accessor 兼容性不稳
- `domain/enums/CustomerSavedViewOperator.java` —— 客户侧八档(与商机六档不同集):in/notIn(多值=逗号分隔约定)/contains/notContains/lt/gt/isNull/isNotNull
- `test/query/CustomerSavedViewFilterTest.java` (14 测试全绿)
- `test/service/impl/CustomerWorkspaceSavedViewIntegrationTest.java` (8 测试全绿:端到端 save→page→恢复、白名单脏条件静默、排序覆盖默认、RECENT 不接、ghost viewId 静默、scope_key 互斥、overview owner in、看板不接)
**修改(6)**:
- `CustomerConstants` + SAVED_VIEW_SCOPE_OVERVIEW/MINE/POOL(customer.* 三值)
- `CustomerWorkspacePageParam` + `savedViewId` —— **落共用基类** :Controller 以基类绑定入参(`POST /{workspace}/page`),issues-11 §D 原拟「三子类各加」在子类不可达,javadoc 已记偏差与理由
- `CustomerWorkspaceQuery` + `savedViewConditions`
- `CustomerWorkspaceServiceImpl` + SavedViewService/CustomerSavedViewFilter 注入 + `applySavedView` (RECENT 内置视图不接,对称商机 RECENT 独立路径;视图排序命中排序池→覆盖 `param.orderBy` 走 PageConverter 既有驼峰转下划线机制)+ `savedViewScopeKey`
- `CustomerMapper.pageWorkspace` 两分支尾部各加 savedViewConditions foreach(看板 SQL 不加,票 10 只接列表分页)
- `CustomerWorkspaceOwnershipIntegrationTest` 构造器同步(构造器新增 2 依赖:mock SavedViewService + 真实 Filter)
### 3.5.2 测试口径备忘(后续改代码别踩)
- **mine 基础集 = owner=self 或协同**:造数只有 owner=self 的客户入集(他人 owner 且无协同行不入);「全量」期望按 workspace 语义算,勿按造数总数算
- 集成测试 SCHEMA 除 customer 外必须含 `customer_team_member` (mine 分支 EXISTS)与 `customer_view_log` (RECENT 分支 EXISTS,即使只测排序也会建触发路径)
- user_saved_view 的 H2 建表 `is_default` 用 `boolean not null default false` (未验证 tinyint(1),boolean 稳)
### 3.5.3 回归确认(第三会话 11:25)
- `test -pl crm-rule,crm-customer -o` **EXIT=0** :crm-rule **146** /0/0、crm-customer **121** /0/0(99 + 票 10 新增 22),21 份报告时间戳全部本次写入,BOM 扫描 clean。
- ⚠ 排查插曲:此前 `test -pl crm-customer -o` (**单模块**)稳定复现 fork discovery 失败,而报告目录曾显示 121 全绿是**历史叠加假象**(surefire 不清旧报告)——已记入陷阱 ⑰。
---
## 3.6 票 11 收工记录(第四会话完成)
### 3.6.1 新增/修改文件
**crm-customer 侧(3 改)**——候选池排除已关联客户(票 04 D-02 语义保持,total 精确):
- `domain/param/CustomerSearchParam.java` + `excludeCustomerIds` (List< Long > 可选)
- `mapper/CustomerMapper.java` searchForOpportunity SQL 内层 `AND id NOT IN <foreach>` (excludeIds 非空才拼)
- `service/impl/CustomerServiceImpl.java` searchForOpportunity 透传 excludeIds
**crm-opportunity 侧(1 依赖 + 1 mapper + 2 新 Port + 切真源 + 注释清理)**:
- `pom.xml` + crm-customer 依赖(单向 opportunity → customer,ADR-0029 范式闭环)
- `mapper/OpportunityMapper.java` +2 @Select :`pageByCustomer`(join opportunity_customer,ORDER BY update_time DESC, id DESC)+ `countActiveByCustomer` (opp_status IN (1,2,3) =「进行中」= 未终结)
- `port/inbound/impl/CustomerOpportunityQueryPortImpl.java` (新建 @Primary 接管 Noop):pageByCustomerId = pageByCustomer + describeStages 批量节点名 + PageResult.convert → OpportunityItem 七组件(estimateAmount 映射 projectAmount);hasActiveOpportunity = count > 0
- `port/inbound/impl/OpportunityOwnerSyncPortImpl.java` (新建 @Primary ):syncOwnerByCustomer(s) = 查存活关联 oppIds(去重)→ 负责人不存在抛 66001(调用方事务回滚,issues-08 §C)→ update(null, wrapper) 三列同步 owner_user_id/owner_name_snapshot/owner_dept_id(部门快照 = DataScope 锚点跟人走;不 bump 乐观锁)
- `service/impl/OpportunitySubServiceImpl.java` :+2 final 字段(ICustomerService/ICustomerContactService);addCustomer 切真源(getById 校验存在性 66001 + customerNameSnapshot 服务端真源覆盖);searchCustomers 全替换(relatedIds → excludeCustomerIds → customerService.searchForOpportunity().convert(toCandidate),补 customerType/ownerName 列);listCustomerContacts 全替换(contactService.listByCustomer 流映射 contactId/name/phone/company/position;null 防御空列表);清理 unused import
- `IOpportunitySubService` / `OpportunitySubController` / 4 个 DTO / `Opportunity` / `OpportunityCustomer` javadoc/@Comment 去「降级 stub/未建」口径(快照口径不变、customerId 必须真实存在)
**测试(2 新类 + 2 类更新)**:
- `port/inbound/impl/CustomerOpportunityQueryPortImplTest` (新建:七组件映射 + stageId null 防御 + describeStages 去重集合捕获 + hasActive 2 态)
- `port/inbound/impl/OpportunityOwnerSyncPortImplTest` (新建:空入参 verifyNoInteractions、无关联 0、负责人不存在 66001、主路径 sqlSet 三列断言)
- `service/impl/OpportunitySubServiceImplTest` :+2 @Mock ;既有 3 个 addCustomer 用例补 getById stub;searchCustomers 用例重写为 excludeIds 传递断言(旧 searchCandidatePool mapper 已不存在);新增 addCustomer_customerMissing + listCustomerContacts 映射/防御 3 用例
- `CustomerContactServiceIntegrationTest` + searchForOpportunity_excludeCustomerIds(NOT IN 排除 + total 同步精确 + 全排除空页)
### 3.6.2 quick-create 接线结论(票面工作项,无代码改动)
商机侧无 quickCreate stub;customer 端点已存在(15 测试覆盖)。票 11 无需接线动作。
### 3.6.3 回归确认(第四会话 13:53)
- `test -pl crm-rule,crm-customer,crm-opportunity -o` **BUILD SUCCESS** :crm-rule **146** /0/0、crm-customer **122** /0/0(121 + 票 11 新增 1)、crm-opportunity **242** /0/0(含票 11 新增 5 用例),报告时间戳 13:51–13:53 全部本次,TOTAL 510/0/0。BOM 扫描 clean(两模块 src+test)。
- 回归形态注意:crm-opportunity 依赖 crm-rule + crm-customer(后者从未 install),三模块必须同 reactor(陷阱 ⑲)。
### 3.6.4 测试口径备忘(后续改代码别踩)
- 不可变 `Map.of(...).get(null)` 抛 NPE → pageByCustomerId 对 stageId null 行防御(测试契约「stageId 空 → 阶段名 null」由实现兑现)
- OwnerSync 测试 wrapper 断言:`getSqlSet()` 含三列 + `getParamNameValuePairs().values()` 含 owner 值三元组(in 集合元素 MP 3.5 展开为独立 param)
- 既有 addCustomer 用例补 `when(customerService.getById(CUSTOMER_A)).thenReturn(customer())` ——Mockito strict stubs 下 missingOppId 用例不补(首个校验即抛)
## 3.7 票 12 进展(第五会话进行中:冒烟闭环 + 前 3 页对齐 + 不符项①已修)
### 3.7.1 冒烟闭环(Blocked by 前提项达成)
- customer_oplog 建表失败(陷阱 ⑳)修复 → fat jar 重打包重启 → **64 表** (customer_oplog + idx_oplog_customer(customer_id, create_time) 就位)→ 冒烟全链 code=0:quick-create(customerNo=KH202609030002、阶段=潜在、星级 3/2、owner快照)→ detail-head → contacts → 商机侧 contacts(票 11 跨模块)→ 关联商机页签(票 11 port)→ 商机侧候选池 66002(商机不存在校验生效)。
- 库检闭环:customer 中文正确落库、customer_oplog 有 CREATE 记录(operator 快照「罗伟健」正确);PS 乱码脏数据行已软删(deleted=1,对应 oplog 保留审计事实)。
- MinIO 启动 WARN「Access Key Id does not exist」——文件模块冒烟受限(远程 bucket key 失效),与本票无关待运维。
### 3.7.2 逐页对齐结论(已核 3/10 页,结论入此)
| 页 | 结论 |
|---|---|
| A4-1-1 公海列表 | 检索(类型/行业)/列集/操作(详情·关注·领取·分配+批量端点)/排序(进入公海时间 grill Q19)符合;**不符①:关联业务计数列缺失**(issues-03 §D 共用列,票 04 漏实现)**→ 已修(3.7.3)**;「客户导出」按钮全规格无票 → 范围外记一笔;关注二态行不回显 = 对称商机口径(前端凭 FOCUSED 视图) |
| A4-2-1 总览/我的客户 | 身份筛选(ASSIGNED/COLLABORATING)/看板三分组(board summary+cards,D20 空列注记)/视图偏好记忆(票 10)符合 |
| A4-4-1 详情+8 页签 | 公共头部 11 项/阶段三态只前进(D20 seam)/跟进弹窗字段校验/页签5·6显--/归档无删除按钮符合;**不符③:关注端点缺失 + is_starred 列缺失 → 已修(3.7.6,拍板 A 全补)**;**不符④:汇总卡 4 张未落 detailHead DTO → 已修(3.7.6,拍板 A 占位恒 null,grill Q18「一律渲染值显--」)**;范围外记一笔:敏感信息三档+查询日志(answer 定稿未纳入)/工商信息更新(企查查 seam)/新增项目·战略协议入口(模块未建) |
| A4-4-8 操作日志 | append-only(§5.5 无限期)/字段级旧→新(detail 叙事,D13 定稿)/时间倒序+日期分组(前端)/导入记 CREATE+UPDATE(§E 值域无 IMPORT)/系统操作不重复记修改符合;**不符⑤:操作类型筛选缺失 → 已修(3.7.6)**(OplogPageParam +action 精确过滤,值域按 §E 定稿 11 种而非原型 15 种);范围外:合并/导出/敏感信息查看日志 |
| A4-7-1 客户交割 | 两段式/reason 字典三值(resign/transfer_post/region_adjust,67011 必填)/D22 接收总监推导(离职自动·调动校验范围)/整批原子 @Transactional/owner 联动(OpportunityOwnerSyncPort+ProjectOwnerSyncPort)/oplog writeSystem 含交接编号全部符合;部分勾选交接 = issues-08 §D 定稿整批简化口径(TransferInitiateDTO 契约,记一笔非不符) |
| A4-3-2 客户导入 | 三方式 APPEND_ONLY/UPDATE_ONLY/UPSERT/模板版本 V1/两段式(DRAFT 预校验→confirm)/行级部分成功/文件预检 67013/状态机 67014 全符合;联系人子表导入 = REVIEW-HANDOFF 明确划出(原型 9.2-9 说 V1 支持,按定稿划出记一笔) |
| A7-3-3-1/2 配置 | 超期提醒:总开关/首次 30 天/二次 7 天/独立开关/全量覆盖单例逐项符合;查重设置:**D21 拍板 ngram seam 覆盖原型编辑距离**(grill Q11,阈值 70/80/90 从配置读)/信用代码硬拦始终生效/D26 电话软提示符合;「宽松/标准/严格」预设档位 = 定稿未纳入(UI 预设,范围外记一笔) |
| 归档领取分配抛公海弹窗 | 前置校验就位:归档阻断 67006/抛公海阻断 67007(port hasActiveOpportunity 票 11 真源)/分配 67012/CAS+oplog+D25 锚点;归档无理由不审批直接执行+提醒排除(D19 收敛)符合;弹窗文案=前端渲染,后端错误语义齐备 |
| A4-3-3-1 新增客户 | CustomerCreateDTO 全字段(工商组/父子客户/总裁班组/confirmSimilar)+查重三层(停输防抖/信用代码硬拦/仍要创建)+阶段默认潜在(冒烟实证)符合;OCR/企查查/职务联动/附件/合并 = map.md D2/D5 有意划出(非不符);**不符②:成员管理写端点缺失**——原型团队成员页 11.1「添加成员(搜索多选)/删除成员」+ issues-07 §40「团队成员变更写日志」,但票 06 只定稿只读 GET members、service 无 addMember/removeMember——规格内部不一致,**已修(3.7.5)**:用户拍板 A(一期补实现 + 注意接口规范) |
### 3.7.3 不符①修复内容(已回归 511/0/0 全绿)
- `CustomerListRowDTO` +opportunityCount/projectCount(Long,插在 vipCustomerLevel 后)
- `CustomerMapper.pageWorkspace` **两分支各** +opportunity 计数标量子查询 + `0 AS project_count` (javadoc 注明:Maven 依赖只能 opportunity→customer,故 SQL 层物理耦合同库表、商机表结构变更须同步 H2 SCHEMA;A5 未建恒 0)
- `CustomerWorkspaceOwnershipIntegrationTest` +listRows_carryOpportunityCount(计 2/逻辑删不计/project 恒 0;⚠ newCust **自带 insert** ,勿外层重插——本次主键冲突虚惊);两 Workspace 测试 H2 SCHEMA 补 opportunity 最小表(id/customer_id/deleted)
- 回归 `test -pl crm-rule,crm-customer,crm-opportunity -o` **511/0/0** (146/123/242,15:06 报告时间戳)BOM clean
### 3.7.4 不符②修复:成员管理写端点(用户拍板 A + 接口规范,回归 516/0/0 全绿)
- **端点**:`POST /api/customer/{id}/members`(form 绑定 MemberAddDTO,memberUserIds 多选,无 @RequestBody )+ `DELETE /api/customer/{id}/members/{memberUserId}` (同模块 CustomerContactController @DeleteMapping 先例,路径参数;ApiPermissionInterceptor fail-open 无需登记)
- **语义**:添加 = 两阶段校验(名单空/含负责人/重复/停用离职 → 67016 整批拒绝,无半写)+ 批内幂等去重 + 姓名快照 + oplog `MEMBER_ADD` ;移除 = 软删 + delete_key 写主键 id(`CustomerTeamMemberMapper.releaseOne`,复用键释放可重复加入)+ oplog `MEMBER_REMOVE` ;负责人不落 team_member 表,传 owner id 自然 67016
- **落点**:CustomerConstants(+67016 CODE_MEMBER_INVALID +2 ACTION)MemberAddDTO(新)/ CustomerTeamMemberMapper(+releaseOne)/ ICustomerDetailService + Impl(+oplogWriter 注入,+addMembers @Transactional/removeMember )/ CustomerDetailController(+2 端点)/ FollowDetail 测试构造同步(writer 提字段,跨方法作用域修复)
- **测试**:CustomerDetailMembersIntegrationTest(H2,5 用例:成功含批内去重/oplog 断言、名单空+客户不存在、两阶段 all-or-nothing、移除软删+复用键重加、移除校验四连);⚠ @TableLogic 下 selectList 只见存活行,软删行断言走原生 SQL queryLong;⚠ customer SCHEMA 必须全列(MP insert 按实体全非 null 字段生成列清单,精简表报 Column is_child not found)
- **回归**:`test -pl crm-rule,crm-customer,crm-opportunity -o` **BUILD SUCCESS 516/0/0** (146/128/242);BOM clean。⚠ 两模块 reactor 复踩陷阱⑲(crm-rule 不在 reactor 缺新类)——必须三模块
### 3.7.5 票 12 剩余工作
- ~~逐页对齐余下 7 页~~ **已完成(10/10 页全核完,结论入 3.7.2 表)**
- ~~不符③④⑤处置~~ **已完成(③④拍板 A,⑤直接落——实施 3.7.6,回归 521/0/0)**
- 对齐全完成后接票 13(接口文档)
### 3.7.6 不符③④⑤修复内容(用户拍板 A/A/A,回归 521/0/0 全绿)
- **③ 关注端点(拍板 A 全补)**:`customer_focus` +`starred` 列(tinyint not null default 0,@Column 钉死列名,ddl-auto update 自动加列);新建 ICustomerFocusService/CustomerFocusServiceImpl/CustomerFocusController——`POST /api/customer/{id}/focus`(幂等 exists 检查,对称 OpportunityCollabServiceImpl)/`/{id}/unfocus`(物理删行)/`/{id}/star`(未关注自动建行 starred=1)/`/{id}/unstar`(保留行仅置 0)+`/focus-batch`(@RequestParam ids,对称商机);一行两态语义:行存在性=关注、starred=重点标记,取关连带清标记(javadoc 注明取舍:避免软删行累积+唯一键复用复杂化);focus/star 校验 67002(对齐 require 口径),unfocus/unstar 幂等不校验(对称商机 unfocus);service 层取 userId(CustomerFollowServiceImpl 范式,Controller 薄);关注/标记不写 oplog(§E 值域无此二者)
- **④ 汇总卡占位(拍板 A 恒 null)**:CustomerDetailHeadDTO +5 字段(wonProjectAmount/planEstimateAmount/ongoingOpportunityAmount/contractAmount/paidAmount,BigDecimal——4 卡 5 值,合同·回款同卡),类 javadoc 改「占位恒 null」口径;detailHead 显式 setNull;ongoingOpportunityAmount 注 TODO 可经 CustomerOpportunityQueryPort 扩展汇总查询后切真值
- **⑤ 日志类型筛选**:OplogPageParam +action(@Schema 列值域 11 种=§E 定稿 9 种+MEMBER_ADD/REMOVE);pageOplogs wrapper +action eq(blank 不过滤);CustomerOplog @Comment 值域同步补齐
- **测试**:CustomerFocusIntegrationTest(H2 custfocus,5 用例:focus 幂等+已标记行不清 starred/unfocus 物理删行+重新关注 starred 归零/star 自动建行+原位置 1/unstar 保留行+行不存在不报错/校验 67002);FollowDetail 测试 detailHead_aggregate +5 占位断言、pageOplogs_filters +action 精确过滤断言
- **回归**:三模块 `test -pl crm-rule,crm-customer,crm-opportunity -o` **521/0/0** (146/133/242,+Focus 5 用例)BOM 零命中;⚠ 中途 javac「需要')'」×6(@Schema 引号转义,坑㉓)修复后重跑
- **冒烟(18:05 全链 code=0)**:fat jar 重打包(⚠ 旧进程锁 jar → repackage rename 失败——**先停 java 进程再打包**)+ 重启 8080;quick-create(⚠ **QuickCreateDTO 无 confirmSimilar 字段** ——商机弹窗场景命中相似客户无法「仍要创建」,票 02/03 范围记一笔;冒烟改不相似名绕开)/ 67002 校验 / focus 幂等 + **star 逐拍新连接验证 starred=1** / unstar 保留行 starred=0 / unfocus 物理删行终态 rows=0 / focus-batch / **detail-head 5 占位全 null** (不符④)/ **oplog action=CREATE 过滤 total=1、action=UPDATE total=0** (不符⑤);starred 列 ddl-auto update 自动加列成功(坑㉕库检快照冻结)
---
### 3.8 票 13 接口文档(交付 `.scratch/customer-module/API-SUMMARY.md`)
- 盘点 9 Controller 55 端点(含票 12 新增 members/focus 组)按**原型界面分组**成表:workspace 三列表看板 / 公海+归属动作 / 新增编辑+查重弹窗结构 / quick-create+search+contacts(商机侧硬依赖)/ 详情头部+8 页签 / oplog(action 11 种值域)/ 成员 / 关注重点标记 / 交割 / 导入 / 提醒配置(crm-rule)/ saved-view(crm-preference)/ 健康检查
- 统一标注:RBAC fail-open + @DataScope 、错误码 67001–67016 总表、脱敏现状(masked/敏感三档未纳入)、雪花 ID 字符串化、写端点 form 绑定(除提醒配置 PUT JSON)
- **§5 已知缺口如实记录**(联调防踩坑):QuickCreateDTO 无 confirmSimilar / 查重设置页无端点 / 页签 5·6 与汇总卡恒 null / 敏感三档·合并·导出划出 / 联系人子表导入
- knife4j 在线为主(@Schema/@Operation 已随各票齐全,与实现签名一致),本文为速查汇总双载体
---
### 3.9 票 14 code-review(两轴:Standards + Spec,fixed point=b48fbb8=HEAD)
- **Standards 轴**:硬违规 0;5 Warnings → 修复 4:① `removeMember` 缺 `@Transactional` (oplog 写失败留半写)② `OpportunityMapper` `pageByCustomer` /`countActiveByCustomer` 漏 `o.deleted=0` (口径分裂,已补)③ `CustomerFocusServiceImpl implements CustomerConstants` 常量接口反模式(全仓首例,改静态引用)④ `TEMPLATE_NAME` 依赖实现类(上移 `ICustomerImportService` ,Controller/Impl 同步切引用);不修 1:`pageWorkspace` 两分支 saved-view 片段重复(Shotgun Surgery,票 10 存量,留票 10 范围内动)
- **Spec 轴**:不符②③④⑤与拍板口径逐项一致 ✅;票 12 十项 checklist 无漏项;API-SUMMARY 抽验 9 端点全一致 ✅;Warnings 2 → 均修:`removeMember`(同 Standards)+ §2.2 漏列 `release-pool-batch` 端点(实形态 `POST /release-pool-batch?ids=` → `BatchResult` ,已补表)
- **回归验证**:修复后重跑三模块 `test -pl crm-rule,crm-customer,crm-opportunity -o` **BUILD SUCCESS 521/0/0** (52 类:146/133/242 持平修复前,review 修复零破坏)+ 全仓 BOM 扫描 688 文件零命中
---
## 4. 踩坑记录(先读再动手;①–⑨ 前会话,⑩–⑯ 第二会话,⑰ 第三会话,⑱–⑲ 第四会话,全部已修复)
| # | 坑 | 现象 | 修法 |
|---|---|---|---|
| ① | MP `BaseMapper.updateById` 返回 `int` | `!mapper.updateById(c)` 编译错 | `if (mapper.updateById(c) <= 0)` ;IService 级才是 boolean |
| ② | batch 的单条方法必须在接口上 | `self.assignOne(...)` 找不到符号 | 提升到 `IXxxService` (票 04 范式) |
| ③ | DTO/Param 包路径 | Param 在 `domain.param` ,DTO 在 `domain.dto` | — |
| ④ | `BaseParam` 分页字段 `current` /`size` | 非 pageNo/pageSize;有默认值 | — |
| ⑤ | `PageResult` 取列表 `getContent()` | 非 getRecords | — |
| ⑥ | Mockito `when(svc.list(any()))` 二义性 | list(Wrapper)/list(IPage) 重载冲突 | `Mockito.<Wrapper<X>>any()` 显式限定 |
| ⑦ | 未 stub 集合方法返回空集合 | 断言失败根因 | 显式 stub |
| ⑧ | `TransferInitiateDTO.reason` 必填 | 直接 initiate → 67011 | 测试 helper 统一设 reason |
| ⑨ | PowerShell 管道后 `$LASTEXITCODE` 误报 | 管道后 =1 但测试全绿 | **以 surefire 报告为准** |
| ⑩ | **commons-io 版本仲裁** :easyexcel-support 3.3.4 传递 2.11.0 近路径胜出 POI 5.2.5 的 2.15.0 | 两级症状:写路径 `NoClassDefFoundError: UnsynchronizedByteArrayOutputStream` (2.12+);钉 2.15.1 后读路径仍 `NoSuchMethodError: FileTimes.isUnixTime` (2.16+,commons-compress 1.27.1 需要) | 根 POM dependencyManagement **钉 2.16.1** (已落,勿动);验证法=PowerShell 对 jar 做常量池字符串搜索 |
| ⑪ | **Write 工具生成 CRLF 行尾** → SearchReplace 多行原文(`\n` 模式)匹配失败 | ` Could not find the exact text`,CR=LF 字节计数证实 | 降级链:**Write 一个 python 脚本文件**(CRLF→LF + str.replace 清单)→ `python 脚本` 执行。⚠ PowerShell 无 heredoc、`python -c` 大载荷会被吞——一律落脚本文件 |
| ⑫ | **Write 批量并发多文件报错是误报** | 报 "no changed to the file" 但 8 个文件实际全部落盘成功 | 先 `Get-ChildItem ... -Filter *Xxx*.java` 验证磁盘真实状态,再决定是否重写;不要盲目重试 |
| ⑬ | **测试引用另一模块新类** :本地仓库的 crm-rule SNAPSHOT jar 是旧版 | crm-customer 测试编译/运行找不到新类 | `-pl crm-customer **-am**` (reactor 内联,直接用 target/classes);`install -o` 离线不可用 |
| ⑭ | **Surefire 3.x 多类指定** :`-Dtest=A,B` 时无匹配的模块报 no tests;`-DfailIfNoTests=false` 参数名不对 | reactor 提前失败 | `"-Dsurefire.failIfNoSpecifiedTests=false"` ;⚠ PowerShell 里含点号的 `-D` 参数**必须整体加引号** |
| ⑮ | surefire 报告/控制台 GBK 编码 | PowerShell 直读中文乱码 | `[System.IO.File]::ReadAllLines(path, [System.Text.Encoding]::GetEncoding('GBK'))` |
| ⑯ | **测试锚点 helper 内含 `now()`** | 多次调用差毫秒 → payload.anchor 比对失真/占位漂移 | 同一客户的锚点**复用同一局部变量**;幂等「不重写」用例锚点选**仅 k=0 到期**的时长(33 天;40 天时 k=1 也到期,Job 正确补发反而使断言失败) |
| ⑰ | ** `test -pl crm-customer -o` (单模块无 -am)fork 内 junit discovery 失败** | EXIT=1 `SurefireBooterForkException: TestEngine with ID 'junit-jupiter' failed to discover tests` ;且 surefire 不清旧报告 → 报告目录历史叠加成「全绿假象」(本次曾把 2 新 + 8 旧误读为 121 全绿) | 回归用 `-pl crm-rule,crm-customer` (双模块,11:25 EXIT=0 证实)或 `-pl crm-customer -am` ;判定绿以报告文件**时间戳 + 逐类计数**为准,勿只看目录总数 |
| ⑱ | **LambdaUpdateWrapper 没有 `.select()`** (列投影是 QueryWrapper 家族 API) | `selectList(new LambdaUpdateWrapper<>().select(...))` 编译错「找不到符号 方法 select」 | 反查投影列用 `LambdaQueryWrapper` (selectList 本就是查询语义);同文件混两种 wrapper 时 import 别抄错 |
| ⑲ | **跨模块测试依赖必须同 reactor** :crm-customer 从未 install(`install -o` 离线不可用——install-plugin 未缓存);`-rf :X` resume 跳过的模块不注册为已解析依赖 | `-pl crm-opportunity -o` 与 `-rf :crm-opportunity` 均 `Could not find artifact com.crm:crm-customer` | 依赖链模块全列出:`test -pl crm-rule,crm-customer,crm-opportunity -o`(1:35,正好票 11 回归范围) |
| ⑳ | ** @Comment 文本内嵌单引号**(CustomerOplog.operatorName「系统动作='系统'」)| Hibernate 生成 DDL 原样拼接 → `create table customer_oplog` MySQL 语法错误(启动仅 WARN,运行期 INSERT 才报表不存在 500)| comment 文本禁含单引号;排查:日志 grep `GenerationTarget` + `Caused by` (SQLError near 乱码段即注释文本)|
| ㉑ | **PS 5.1 Invoke-RestMethod -Body byte[](UTF8.GetBytes)中文 form 仍被按系统代码页(GBK)重编码** | 服务端按 charset=utf-8 解码得 mojibake 入库(pymysql 库检「鍐掔儫…」实锤);**服务端无错**——python urllib 同接口 roundtrip 正确 | 冒烟/联调带中文一律用 python `urllib` (tmp/smoke-t12g.py 范式:urlencode+utf-8、pymysql 查库闭环)或 curl.exe,勿用 PS 发中文 form |
| ㉒ | **SearchReplace replace_all=true 实际只替换第一处** | 多处相同锚点(pageWorkspace 两分支同文本 SELECT 段)只改第一处 → 第二分支机会性漏改,测试 null 断言暴露 | 同文本多处时**逐处唯一化锚点分开替换**;改完 grep 锚点计数核验(本次 opportunity_count 应 2 处实为 1 处)|
| ㉓ | **注解 description 内嵌转义引号(显 "--")经 edit 工具 JSON 序列化,转义层次不可控** | CustomerDetailHeadDTO 6 行落盘成裸引号/双反斜杠 → javac「需要')'」×6;第二轮 SearchReplace 转义再猜错 | 特殊字符替换走 **python 脚本 + chr(92)/chr(34) 构造字节序列** (.scratch/fix_quotes.py 范式,JSON 参数零反斜杠无歧义),一次修 6 处 |
| ㉔ | ** 「save file failed, reason: unknown」假失败真写入(本会话高频 5+ 次)** | Write/SearchReplace 报保存失败但实际已落盘;盲目重试 → 双行写入(CustomerConstants 曾重号) | 纪律:**报错先 Read/Grep 验证再决定**,已写入则跳过,未写入才重试 |
| ㉕ | **pymysql 默认 autocommit=False → REPEATABLE READ 冻结快照** | 冒烟同连接连续库检呈「滞后一拍」假象(star 后查得 starred=0、unfocus 后查得 rows=1),端点实际全部生效(终态新连接验证 rows=0) | 冒烟库检**每次新连接或 autocommit=True**;中间态断言独立脚本(tmp/smoke-t12m2.py 范式) |
历史坑(仍适用):MP `updateById` 默认 NOT_NULL 策略跳过 null 字段(清空须 `update(entity, wrapper)` + set(col, null),wrapper 自带 eq(id));高频副作用更新不 bump version 用 `update(null, wrapper)` ;`@TableLogic` 软删行查询不可见;H2 SCHEMA 必须含实体**全部列**(含 BaseEntity 审计列);`.java` UTF-8 无 BOM;孤立单大写字母字段必须 `@Column(name=...)` 钉死(AGENTS.md)。
---
## 5. 构建 / 测试命令(本机实证路径,旧文档的 G:\ / IDEA 2025.1.3 / e:\ 均不存在)
**PowerShell 用 `;` 不用 `&&` 。**
```powershell
# 编译(多模块)
$env:JAVA_HOME='D:\IntelliJ IDEA 2023.3.8\jbr'; & d:\code\crm-backend-matt\tmp\apache-maven-3.9.9\bin\mvn.cmd -q -s d:\code\crm-backend-matt\settings.xml compile -pl crm-rule,crm-customer -o 2>& 1 | Select-Object -Last 15; "EXIT=$LASTEXITCODE"
# 单测试类(单模块)
$env:JAVA_HOME='D:\IntelliJ IDEA 2023.3.8\jbr'; & d:\code\crm-backend-matt\tmp\apache-maven-3.9.9\bin\mvn.cmd -q -s d:\code\crm-backend-matt\settings.xml test -pl crm-customer "-Dtest=CustomerImportIntegrationTest" -DfailIfNoTests=true -o
# 测试引用另一模块新类时(陷阱 ⑬⑭)
... test -pl crm-customer -am "-Dtest=CustomerReminderJobIntegrationTest" "-Dsurefire.failIfNoSpecifiedTests=false" -o
# 结果以 surefire 报告为准(陷阱 ⑨⑮)
[System.IO.File]::ReadAllLines('d:\code\crm-backend-matt\crm-customer\target\surefire-reports\com.crm.customer.service.impl.CustomerImportIntegrationTest.txt', [System.Text.Encoding]::GetEncoding('GBK')) | Select-Object -First 5
# 全模块计数汇总
$tot=0;$fail=0;$err=0; Get-ChildItem d:\code\crm-backend-matt\crm-customer\target\surefire-reports\*.txt | ForEach-Object { $l=([System.IO.File]::ReadAllLines($_.FullName,[System.Text.Encoding]::GetEncoding('GBK')) | Where-Object { $_ -match 'Tests run:' } | Select-Object -First 1); if ($l -match 'Tests run: (\d+), Failures: (\d+), Errors: (\d+)') { $tot+=[int]$Matches[1]; $fail+=[int]$Matches[2]; $err+=[int]$Matches[3] } }; "total=$tot failures=$fail errors=$err"
# BOM 扫描(每票收工必跑;排除 target)
$bad = Get-ChildItem -Path d:\code\crm-backend-matt -Recurse -Filter *.java | Where-Object { $_.FullName -notmatch '\\target\\' } | ForEach-Object { $b=[IO.File]::ReadAllBytes($_.FullName); if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) { $_.FullName } }; if ($bad) { $bad } else { 'BOM scan clean' }
```
---
## 6. 接手建议顺序
1. 读本文第 3.6 节(票 11 收工)→ 读 `tickets/12-*.md` + `tickets/13-*.md` (收尾票,勿凭记忆)。
2. 票 12+13 收尾:全量测试(含 crm-app 装配冒烟)+ 文档核对(CONTEXT.md / 接口说明书)。
3. 票 14 code-review(/code-review 两轴)+ git 提交(**不用** /commit 类 skill 跳过 review)。
4. 每票收工固定动作:`mvn compile` → H2 集成测试跑绿 → BOM 扫描 → 更新本档。
**TodoWrite 当前状态**:t01–t11 全部 COMPLETE(票 11 第四会话三模块回归 146+122+242=510 全绿后收尾,见 3.6.3);t12+13/t14 PENDING。
---
## 7. Suggested skills
| 场景 | skill |
|---|---|
| 继续实现(默认) | `/implement .scratch\customer-module` (本次流水线的主 skill) |
| 票 10/11 设计有疑义需要压测决策 | `/grill-me` 或 `/grill-with-docs` (后者同步产出 ADR + 术语表) |
| 票 11 商机侧 Port 真实现涉及跨模块架构 | `/codebase-design` (deep module 词汇)或 `/domain-modeling` (记 ADR) |
| 票 12–13 收尾前自查 | `/code-review` (两轴:Standards + Spec;spec 轴对照 `tickets/*.md` ) |
| 收尾提交 | 直接 git(**不要**用 `/commit` 类 skill 跳过 review);用户明确要求过 code-review 后再提交 |
| 卡住/回归排障 | `/diagnosing-bugs` |
**不要**调用:`/handoff`(本文件即是产物,避免套娃)、`/to-spec`/`/to-tickets`(spec 与 13 票已定稿冻结,重开会推翻已拍板的 D1–D29)。
---
## 8. 参考路径速查
- 决策索引:`.scratch/customer-module/map.md`(D1–D29 自包含)
- 票清单:`.scratch/customer-module/tickets/01..13-*.md` + `README.md` (通用要求:UTF-8 无 BOM / 孤立单大写字母检查 / 依赖方向单向 opportunity→customer / 每票 mvn compile + H2 集成测试)
- 逐票定稿:`.scratch/customer-module/issues/NN-answer.md`(票 08=`09-answer.md`、票 09=`10-answer.md`,文件名与票号差 1)
- 工程纪律:`AGENTS.md`(BOM 规则、JPA 命名策略陷阱、edit 工具降级、stream dropout 应对)
- 领域上下文:`crm-customer/CONTEXT.md`、`crm-rule/CONTEXT.md`(票 09 已补客户规则子域段)、`CONTEXT-MAP.md`
- ADR:`docs/adr/0018`(per-module data-scope)、`0021`(CAS)、`0022`(读写分离深模块)、`0023`(批量与视图统计)、`0024`(Bruno 分组)、`0028`(BatchRunner)、`0029`(port 单向依赖解环)
- 对称参照实现:`crm-opportunity`(商机侧 Job/待发通知/规则单例三范式源头:OpportunityMaintenanceJob / OpportunityPendingNotice / OpportunityPoolRuleServiceImpl);`crm-lead`(self 代理范式源头)
- 测试范式源头:`crm-customer/src/test/.../CustomerImportIntegrationTest.java`(843 行,最新最全骨架:5 表 SCHEMA + EasyExcel 造数 + FileApi 桩);`crm-customer/src/test/.../job/CustomerReminderJobIntegrationTest.java`(Job + crm-rule 跨模块测试范式)