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.
 
 
 
 
 

40 KiB

实现进度交接 — 客户模块 /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.mdcrm-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_defaultboolean 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 可选)
  • 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/countActiveByCustomero.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 文件零命中
  • 跨机交接包HANDOFF-CROSS-MACHINE-2026-09-03.md + tools/(冒烟/回归脚本脱敏版,凭据走环境变量)已入库随 git 同步,换机器以该包为准

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-opportunityCould 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 用 ; 不用 &&

# 编译(多模块)
$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.mdcrm-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 跨模块测试范式)