16 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 客户导入 | 🚧 本会话进行中(约 85%) | 0 | 详见第 3 节,接手第一件事=编译 |
| 09 crm-rule 客户规则族 + Job | ⏳ | — | 依赖票 06 锚点 last_valid_follow_time |
| 10 saved-view 接入 | ⏳ | — | 3 scope_key + CustomerSavedViewFilter |
| 11 商机侧切真源 | ⏳ | — | CustomerOpportunityQueryPort/StagePort/OwnerSyncPort 真实现替换 Noop |
| 12+13 收尾 | ⏳ | — | 全量测试 + 冒烟 + 文档核对 |
| 14 code-review + 提交 | ⏳ | — | — |
crm-customer 模块测试总数:75 全绿(7+17+8+17+12+14,票 07 收工时)。 crm-dict DictDataInitializerTest:8 全绿(24 组 / 229 项,票 07 加 transfer_reason)。
2. 票 07 收工记录(本会话完成,验收全过)
新增/修改文件:
crm-customer/.../controller/CustomerTransferController.java(6 端点,issues-08 §D 契约)crm-customer/.../service/impl/CustomerTransferServiceImpl.java(两段式核心;本会话修了 3 处编译错,见第 4 节陷阱 ①②③)crm-customer/.../service/ICustomerTransferService.java(加assignOne到接口——票 04 范式:单条动作必须在接口上,batch 才能经self.走事务代理)crm-customer/.../test/.../CustomerTransferIntegrationTest.java(12 测试)crm-dict/.../config/DictDataInitializer.java(+transfer_reason 组,sortNo 24;code 用语义码 resign/transfer_post/region_adjust,非_NN形态——因 TransferInitiateDTO 的 @Schema 已写死该口径)crm-dict/.../config/DictDataInitializerTest.java(9 处断言同步:23→24 组、226→229 项、188→191 项)
票 07 设计定案(实现已落,供后续票参考):
preview()无参(issues-08 §D 写的{id}/preview是笔误——发起前还没有交接单 id);服务/Controller 均为GET /transfer/previewinitiate不带 customerIds:系统按「当前用户名下可交接客户」整批计算(与 preview 同口径,防前端传值不一致)assign批量走BatchRunner.run(customerIds, id -> self.assignOne(...), ...);self是ICustomerTransferService(接口,非 Impl)- oplog 全部
writeSystem(操作人显「系统」+ detail 带交接编号,issues-08 §F) - D22 总监推导 =
sys_dept.leader_user_id(不改 crm-auth);D23 自动完成 =assignedCount == totalCount → status=1
3. 票 08 当前状态(🚧 接手重点)
3.1 已落盘文件(13 个 + pom)
已逐个核验存在,但全部未经编译验证;controller/CustomerImportController.java 核验为 MISS(待写)。
crm-customer/pom.xml +easyexcel(父 POM 管版本 4.0.3)
.../domain/entity/CustomerImportTask.java ⚠ 覆盖了旧版(87 行,见 3.3)
.../domain/entity/CustomerImportFail.java
.../mapper/CustomerImportTaskMapper.java
.../mapper/CustomerImportFailMapper.java
.../domain/excel/CustomerImportRow.java 客户 sheet 行(9 列 @ExcelProperty index)
.../domain/excel/ContactImportRow.java 联系人 sheet 行(5 列)
.../domain/dto/ImportPreviewDTO.java 含内嵌 Row
.../domain/dto/ImportResultDTO.java
.../domain/param/ImportPageParam.java
.../service/ICustomerImportService.java 6 方法(upload/confirm/result/failures/page/template)
.../service/impl/CustomerImportAnalyzer.java ★ 判定器(upload 与执行共用,297 行)
.../service/impl/CustomerImportServiceImpl.java 349 行
.../task/CustomerImportExecutor.java @Async 执行器,328 行
3.2 待办(按序)
controller/CustomerImportController.java未写(上一次 Write 被取消)。6 端点(issues-09 §D):POST /api/customer/import/upload(MultipartFile + @RequestParam importMode)/POST /{taskId}/confirm/GET /{taskId}/GET /{taskId}/failures/GET /page/GET /template(返回ResponseEntity<byte[]>,对称crm-file/controller/FileController.thumbnail风格:ContentDisposition + xlsx MediaType + ContentLength)。- 编译(票 08 所有新文件从未编译过——最大风险点)。
- H2 集成测试
CustomerImportIntegrationTest(骨架抄CustomerTransferIntegrationTest:H2 mem + MybatisConfiguration + MetaObjectFillHandler + ReflectionTestUtils 填 baseMapper/entityClass)。 SCHEMA 需要:customer(全列 DDL,抄票 07 测试的 SCHEMA)+customer_contact+customer_oplog+customer_import_task+customer_import_fail。 mock:SecurityUtils(静态)、IAuthUserService、ISysDeptService、CustomerNameSimilarityPort(桩:返回命中即触发 SUSPECT)、FileApi(upload→FileInfoDTO.fileId;download→FileDownloadDTO 带 ByteArrayInputStream)。 Executor 是 @Component 无 self 代理——测试直接 new + 反射填 9 个依赖 + 直调executeImport(@Async 无代理时天然同步,不 flaky)。 建议覆盖:模板表头校验拒绝 / 三模式判定(APPEND_ONLY 命中→FAIL「客户已存在」;UPDATE_ONLY 未命中→FAIL「未找到可更新客户」;UPSERT 名称相似→SUSPECT 不写入)/ 编号与信用代码命中不同客户→冲突 FAIL / 文件内多行匹配同一客户→重复组全 FAIL(Q14)/ 更新空白单元格不改值 + 值相同计 unchanged / 联系人 D26 四规则(本客户电话→UPDATE;他客户→照常 INSERT + suspect 明细;电话空同名同职务→SUSPECT;同文件同客户同电话两行→FAIL)/ 联系人失败不回滚主记录 / 状态机 DRAFT→RUNNING→DONE + 重复 confirm 67014 / page 仅本人任务。 - BOM 扫描 + 全模块测试 +
mvn compile。 - 票 08 不涉及 crm-dict(无新字典分组)。
3.3 关键设计决策(本会话拍定,实现已落)
- 文件存储走 crm-file:upload 时
fileApi.upload(bytes, name, contentType, "customer-import")→ 任务只存fileFileId(String)+fileHash(SHA-256 hex)。执行时fileApi.download(fileId)重新取流解析。 ⚠CustomerImportTask.java原有旧版(存fileName+failDetailJsonlongtext)已被本会话覆盖——旧版没有文件引用,执行时无内容可解析,属设计缺陷;失败明细改落独立表customer_import_fail(可分页/下载,优于 JSON blob)。 - 模板版本锚点 = 表头文本逐列比对(不用单独锚点行):
CUSTOMER_HEADERS9 列常量,任一列不符 → 67013「模板版本不匹配」。模板生成 = 双 sheet(客户/联系人)纯表头无示例行(示例行会被当数据读入)。 - 判定与执行分离但共用同一 Analyzer:预计数与实际结果同口径(避免「预校验说能导、执行却失败」)。
- 异步无登录态:
MetaObjectFillHandler用SecurityUtils.getUserId()(非 getRequired),无登录态审计字段留 null 属正常(该类注释明说);owner 快照从task.creatorId反查userService.getById;客户级 oplog 用writeSystem+ detail 带任务 ID。 - 导入创建客户:
ownerUserId= 提交人(issues-09 §C 普通销售固定)、customerStage= STAGE_POTENTIAL(不得指定)、编号行内优先否则nextCustomerNo()续号(KH+yyyyMMdd+4 位,对称交割 JG)。 - 更新白名单列:customerType/provinceCode/cityCode/industryCode/customerStarLevel/remark(负责人/阶段/工商字段/信用代码不入模板 → 天然禁改)。空白 = 不修改;值相同 =
unchangedCount++不写库。 - 疑似重复双义:客户行 SUSPECT(名称相似)= 不写入 + suspectCount;联系人他客户电话命中 = 照常写入 + suspectCount + fail 表明细(D26「软提示不禁止导入」)。fail 表 reason 以「疑似重复」前缀区分。
- 联系人挂接:
customerIdByNo映射(新增行落库后回填 / 命中行直接放);归属客户未成功导入 → 联系人行 FAIL「归属客户未成功导入」。
4. 本会话新踩坑(务必先读,全是已修复的编译/测试错)
| # | 坑 | 现象 | 修法 |
|---|---|---|---|
| ① | MP BaseMapper.updateById 返回 int(非 boolean) |
!customerMapper.updateById(c) → 「一元运算符 '!' 的操作数类型 int 错误」 |
if (customerMapper.updateById(c) <= 0)。注意 IService.updateById(this.updateById)返回 boolean——mapper 级与 service 级签名不同 |
| ② | batch 的单条方法必须在接口上 | self.assignOne(...) 找不到符号(self 声明为接口类型) |
把 assignOne 提升到 ICustomerTransferService(票 04 范式:claim/assign/releasePool 等单条动作全在接口) |
| ③ | DTO/Param 包路径 | import ...domain.dto.TransferPageParam 找不到 |
Param 类在 domain.param 包(TransferPageParam/ImportPageParam 等),DTO 在 domain.dto |
| ④ | BaseParam 分页字段是 current/size(不是 pageNo/pageSize) |
测试 setPageNo(1L) 编译错 |
有默认值(current=1/size=10),通常不用设 |
| ⑤ | PageResult 取列表是 getContent()(非 getRecords) |
测试编译错 | PageResult 字段:content/total/size/current/pages/empty |
| ⑥ | Mockito when(svc.list(any())) 二义性 |
「对 list 的引用不明确」(IService 有 list(Wrapper) 与 list(IPage) 重载) |
Mockito.<Wrapper<AuthUser>>any() 显式限定 |
| ⑦ | Mockito 未 stub 的集合方法返回空集合 | userService.list(...) 未 stub → assignable 列表恒空,断言失败 |
显式 stub;这是 assignableUsers 测试失败的根因 |
| ⑧ | TransferInitiateDTO.reason 必填 |
测试 new TransferInitiateDTO() 直接 initiate → 67011「交接原因必填」 |
测试统一走 initiateCmd() helper 设 reason="resign" |
| ⑨ | mvn 输出经 PowerShell 管道后 ExitCode 误报 | Select-Object -Last N / Select-String 后 $LASTEXITCODE=1 但测试其实全绿 |
以 surefire 报告为准:Get-Content target\surefire-reports\<类全名>.txt 看 Tests run: N, Failures: 0, Errors: 0 |
历史坑(前几次会话,仍适用):MP updateById 默认 NOT_NULL 策略跳过 null 字段(清空字段须 update(entity, wrapper) + LambdaUpdateWrapper.set(col, null),wrapper 必须自带 eq(id));高频副作用更新不 bump version 用 update(null, wrapper);@TableLogic 软删行查询不可见(验证软删须原生 JDBC);H2 测试 SCHEMA 必须含实体全部列(含 BaseEntity 审计列 creator_id/updater_id/deleted);.java 必须 UTF-8 无 BOM。
5. 构建 / 测试命令(本机固定范式)
mvn 不在 PATH;JAVA_HOME 指向 IDEA 自带 JBR。PowerShell 用 ; 不用 &&。
# 编译(多模块)
$env:JAVA_HOME='D:\IntelliJ IDEA 2025.1.3\jbr'; & G:\apache-maven-3.8.4\bin\mvn.cmd -q -s e:\code\crm-backend-matt\settings.xml compile -pl crm-customer,crm-dict -o 2>&1 | Select-String "ERROR|BUILD" | Select-Object -First 8; echo "EXIT=$LASTEXITCODE"
# 单测试类
$env:JAVA_HOME='D:\IntelliJ IDEA 2025.1.3\jbr'; & G:\apache-maven-3.8.4\bin\mvn.cmd -q -s e:\code\crm-backend-matt\settings.xml test -pl crm-customer "-Dtest=CustomerTransferIntegrationTest" -DfailIfNoTests=true -o 2>&1 | Select-Object -Last 12
# 结果以 surefire 报告为准(见陷阱 ⑨)
Get-Content "E:\code\crm-backend-matt\crm-customer\target\surefire-reports\com.crm.customer.service.impl.CustomerTransferIntegrationTest.txt" | Select-Object -First 4
# 全模块测试
... test -pl crm-customer -o
Select-String -Path "E:\code\crm-backend-matt\crm-customer\target\surefire-reports\*.txt" -Pattern "Tests run:" | ForEach-Object { $_.Line }
# BOM 扫描(每票收工必跑)
$files = Get-ChildItem -Recurse -Filter *.java -Path "E:\code\crm-backend-matt\crm-customer\src","E:\code\crm-backend-matt\crm-dict\src"; $bad = @(); foreach ($f in $files) { $b = [IO.File]::ReadAllBytes($f.FullName); if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) { $bad += $f.FullName } }; echo "TOTAL=$($files.Count) BOM_BAD=$($bad.Count)"
编译错误定位技巧:2>&1 | Select-String "ERROR.*\.java" | Select-Object -First 10(只取带文件名的错误行,避开 PowerShell 中文乱码噪声)。
6. 接手建议顺序
- 读本文第 3、4 节 → 读
tickets/08-customer-import.md+issues/09-answer.md(定稿,勿凭记忆)。 - 写
CustomerImportController.java(3.2 第 1 项)。 mvn compile -pl crm-customer -o→ 按第 4 节陷阱表逐个修(预期有编译错,14 个新文件从未编译)。- 写
CustomerImportIntegrationTest(3.2 第 3 项的用例清单)→ 单类跑绿 → 全模块跑绿(预期 75 + 新增)。 - BOM 扫描 → TodoWrite 置 t08 COMPLETE → 票 09。
- 票 09–14 依次推进(票 09 起要动
crm-rule,注意其 CONTEXT.md 与既有单例范式)。
TodoWrite 当前状态:t01–t07 COMPLETE(含 t04a–t04e 子任务)、t08-import IN_PROGRESS、t09–t14 PENDING。
7. Suggested skills
| 场景 | skill |
|---|---|
| 继续实现(默认) | /implement .scratch\customer-module(本次流水线的主 skill,base: C:\Users\Administrator\.agents\skills\implement) |
| 票 08 测试想用 TDD 补齐(Analyzer 判定逻辑是天然 seam) | /tdd |
| 票 08/09 设计有疑义(如联系人挂接边界、规则族单例形态)需要压测决策 | /grill-me 或 /grill-with-docs(后者会同步产出 ADR + 术语表) |
票 09 要在 crm-rule 落「客户规则族」单例,涉及跨模块架构决策 |
/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 - 工程纪律:
AGENTS.md(BOM 规则、JPA 命名策略陷阱、edit 工具降级、stream dropout 应对) - 领域上下文:
crm-customer/CONTEXT.md、CONTEXT-MAP.md - ADR:
docs/adr/0018(per-module data-scope)、0021(CAS)、0022(读写分离深模块)、0023(批量与视图统计)、0024(Bruno 分组)、0028(BatchRunner)、0029(port 单向依赖解环) - 对称参照实现:
crm-opportunity(商机图已完成)、crm-lead(线索状态机 + self 代理范式源头) - 测试范式源头:
crm-customer/src/test/.../CustomerWorkspaceOwnershipIntegrationTest.java(703 行,H2 + mock auth + ReflectionTestUtils 全套骨架)