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.
 
 
 
 
 

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/preview
  • initiate 不带 customerIds:系统按「当前用户名下可交接客户」整批计算(与 preview 同口径,防前端传值不一致)
  • assign 批量走 BatchRunner.run(customerIds, id -> self.assignOne(...), ...)selfICustomerTransferService(接口,非 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 待办(按序)

  1. 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)。
  2. 编译(票 08 所有新文件从未编译过——最大风险点)。
  3. 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(静态)、IAuthUserServiceISysDeptServiceCustomerNameSimilarityPort(桩:返回命中即触发 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 仅本人任务。
  4. BOM 扫描 + 全模块测试 + mvn compile
  5. 票 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 + failDetailJson longtext)已被本会话覆盖——旧版没有文件引用,执行时无内容可解析,属设计缺陷;失败明细改落独立表 customer_import_fail(可分页/下载,优于 JSON blob)。
  • 模板版本锚点 = 表头文本逐列比对(不用单独锚点行):CUSTOMER_HEADERS 9 列常量,任一列不符 → 67013「模板版本不匹配」。模板生成 = 双 sheet(客户/联系人)纯表头无示例行(示例行会被当数据读入)。
  • 判定与执行分离但共用同一 Analyzer:预计数与实际结果同口径(避免「预校验说能导、执行却失败」)。
  • 异步无登录态MetaObjectFillHandlerSecurityUtils.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.updateByIdthis.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\<类全名>.txtTests 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. 接手建议顺序

  1. 读本文第 3、4 节 → 读 tickets/08-customer-import.md + issues/09-answer.md(定稿,勿凭记忆)。
  2. CustomerImportController.java(3.2 第 1 项)。
  3. mvn compile -pl crm-customer -o → 按第 4 节陷阱表逐个修(预期有编译错,14 个新文件从未编译)。
  4. CustomerImportIntegrationTest(3.2 第 3 项的用例清单)→ 单类跑绿 → 全模块跑绿(预期 75 + 新增)。
  5. BOM 扫描 → TodoWrite 置 t08 COMPLETE → 票 09。
  6. 票 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.mdCONTEXT-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 全套骨架)