# CRM 客户模块缺陷修复报告(D-02 / D-03 / D-04 / D-05 / D-07) - effort:`.scratch/customer-defectfix/`(customer-defectfix) - 日期:2026-09-06 - 基线:customer-e2e-r2(233 例 ✅221 ⚠9 ❌3,`e2e-report-v2.md` §一) - 构建与验证环境:corretto-17(`C:\Users\Administrator\.jdks\corretto-17.0.13`)+ verify profile + CRM_MINIO_AK/SK;E2E 解释器 `py -X utf8`(系统 py 3.13.3) - 契约权威:`.scratch/customer-module/API-SUMMARY.md`(73 端点);本次 5 条修复全部未触碰端点路径/方法/绑定形态(ADR-0017 红线合规) --- ## 一、结论速览 | 缺陷 | 级别 | 修复 | 单测 | E2E 验收 | 手工复核 | |---|---|---|---|---|---| | D-05 导入 INSERT 行必 FAILED | P1 | ✅(含同族 relation_star_level) | ✅ 19/19 | ✅ heavy 14.11/12b/13 全转 ✅ | ✅ ALL_OK(2 行 INSERT 落库) | | D-07 assignable 稳定超时 | P1 | ✅(两处根因都修) | —(E2E 断言) | ✅ heavy 13.10 ✅ 6046 人 | ✅ 0.42s / 0.40s(目标 <5s) | | D-04 失败明细全丢 | P2 | ✅ clip500 | ✅ 新增 600 字符用例 | ✅ 行级明细 8 行齐全 | ✅ reason 恰 500 字符实证 | | D-02 跟进过去时间未拦截 | P2 | ✅ 67009 | —(E2E 断言) | ✅ core F-08 转 ✅ | — | | D-03 缺 version 报 67001 | P2 | ✅ 改 67005 | —(E2E 断言) | ✅ core F-05 转 ✅ | — | **终局 E2E**:core 112 例 ✅111 ⚠1 ❌0 | heavy 61 项 ✅60 ⚠0 ❌1(唯一 ❌=teardown 瞬时远程库断连,非断言,重试自愈)| incr 63 例 ✅61 ⚠2 ❌0。**断言口径 ❌=0**,残余 ⚠3 全为任务书预期的非缺陷类。 --- ## 二、逐缺陷修复明细 ### D-05(P1)导入含 INSERT 行必 FAILED - **根因**:`createCustomer()` 未给 `is_biz_negotiated` 赋值;远程共享库该列 `tinyint not null` 无默认 → 单行 INSERT 报 `Field 'is_biz_negotiated' doesn't have a default value`,连锁 D-04 把任务打成 FAILED 且明细全丢。UPDATE_ONLY 不受影响(`applyUpdate()` 不触碰该列,现状即对,未改动)。 - **位置**:`crm-customer/src/main/java/com/crm/customer/task/CustomerImportExecutor.java` L247-252(`customerMapper.insert(c)` L254 之前)。 - **diff 摘要**:insert 前补三行缺省—— - `c.setIsBizNegotiated(0)`(任务书最小修复;口径同 quick-create「商务洽谈=否」) - `c.setIsChild(0)`(顺手显式化,与列 default 0 语义一致,无害) - `c.setRelationStarLevel(0)`(**修复中发现并实锤的同族问题**:`relation_star_level` 同为 NOT NULL 无默认,且导入模板 9 列无「关系星级」可填——任何 INSERT 行必 null 必炸。0=「未评估」占位,编辑时 `requireStar` 强制重选 1~5,可自愈) - **可选加固(待批,未执行)**:`deploy/migration-d05-is-biz-negotiated-default.sql`——`is_biz_negotiated` 补 `default 0`;`relation_star_level` 放开 NOT NULL 的注释块草案。共享库纪律:未征得同意不动 DDL。 - **验证**: - 单测 `CustomerImportIntegrationTest.confirm_insertMode_e2eCreatesCustomer` 补 biz/isChild/relationStar 三断言,19/19 绿; - heavy F-14 主任务终态 DONE `insert=2(客户r1+联系人甲) fail=7 suspect=1`;14.13 DB 实证 r1 落库 `biz=0 rel=0`; - 手工复核(§六)二轮 ALL_OK:模板下载 → 2 行 INSERT → APPEND_ONLY → confirm → DONE → DB `is_biz_negotiated=0 AND is_child=0`。 ### D-04(P2)导入失败明细全丢 - **根因**:`insertFail()` / `markFailed()` 对 reason 不截断;超长 MyBatis 异常文本写入 `customer_import_fail.fail_reason` / `customer_import_task.fail_reason`(均 varchar(500))报 `Data too long`,被 `doImport` catch-all 吞掉 → 任务 FAILED 且 fail 表 0 行。 - **位置**:同文件 L299-319;新增私有 `clip500` L321-327(≤500 直接返回;否则截 497 + `...`,MySQL varchar 按字符计)。 - **diff 摘要**:`insertFail` L310 `f.setFailReason(clip500(reason))`;`markFailed` L316 `task.setFailReason(clip500(reason))`。正常长度 reason 原样透传,无行为变化。 - **验证**: - 单测新增「600 字符 reason → 落库成功且长度 ≤500」用例(同测试类 19/19); - E2E:heavy 14.12 失败明细 8 行齐全(客户 5 + 联系人 2 + SUSPECT 提示行 1),任务不再整体 FAILED; - **手工复核首轮意外实证**:构造缺省/市/行业的 INSERT 行 → 每行失败 reason 恰 500 字符成功落库(内容为 `Field 'city_code' doesn't have a default value` 的完整 MyBatis 异常被截断),任务 DONE、明细可见——修复前该路径必 Data too long 二次炸。 ### D-07(P1)交割 assignable 接口稳定超时 - **根因(两处,都修)**: 1. `CustomerTransferServiceImpl.assignableUsers()` 循环内逐用户 `deptService.getDeptNames(Set.of(u.getDeptId()))`(原 L127)——每用户一次 DB 往返,远程库 RTT 放大; 2. `SysDeptServiceImpl.getChildDeptIds()` `list()` 全表加载 + `collectChildren` O(n²) 递归(原 L112-136)。 - **位置与 diff 摘要**: 1. `crm-customer/.../service/impl/CustomerTransferServiceImpl.java` L109-138:先收集候选部门 `Set`,循环前一次 `getDeptNames(candidateDeptIds)` 批量取,循环内查 Map(L121-124);范围/过滤/排序语义零变化。 2. `crm-auth/.../service/impl/SysDeptServiceImpl.java` L119-137:利用 `SysDept.ancestors` 祖级链改单条前缀查询——`chain = ancestors + "," + deptId`,命中 `eq(chain) or likeRight(chain + ",")`(L127-135);ancestors 缺失时保留全表递归兜底。接口签名不动,另两个生产调用方 `UserQueryServiceImpl`(users/page / users/stats)对内部优化透明。 - **计时对比**(探测脚本 `probe-assignable.py`,同参数同数据): | 时点 | 耗时 | 返回人数 | 说明 | |---|---|---|---| | 修复前 probe-1 | 53.52s(30s×2 超时稳定复现) | — | E2E 与手动 probe 双复现 | | 修复前 probe-2 | 52.03s | 6045 | | | 修复后 probe-1 | **0.42s** | 6045 | 与 before 人数完全一致 | | 修复后 probe-2 | **0.40s** | 6045 | 加速 ≈127×,目标 <5s、争取 <1s 均达成 | - **影响面冒烟**(`smoke-userlist-collab.py`):`POST /api/system/users/page` 0.16s(total=6048,主部门名解析正常)、`GET /api/system/users/stats?deptId=本部` 0.74s(deptCount=812)。 - **E2E**:heavy 13.10 转 ✅(断言前把销售甲临时挂入总监子树、断言后立即还原——seed「华南销售部」parent_id=0 为独立顶层树,round-1/r2 该断言因超时 warn 从未真正执行,详见 §八用例侧)。 ### D-02(P2)跟进 nextFollowTime 早于当前未拦截 - **根因**:`CustomerFollowServiceImpl.addFollow()` 只校验 followWay/followContent 空白,无 nextFollowTime 时间校验;append-only 无改删口 → 永久超期提醒。常量 `CODE_FOLLOW_INVALID=67009` 已存在。 - **位置**:`crm-customer/.../service/impl/CustomerFollowServiceImpl.java` L62-66(校验块在任何写库之前,事务首段)。 - **diff 摘要**:`dto.getNextFollowTime() != null && isBefore(now())` → `BusinessErrorException(67009, "下次跟进时间不能早于当前时间")`(严格早于,等于当前不拦)。 - **验证**:core F-08 负路径(传过去时间 → 67009)转 ✅;正路径(未来时间)与 demo 现有流程不受影响;事务/锚点刷新逻辑未动。 ### D-03(P2)编辑缺 version 报 67001,spec 约定 67005 - **根因**:`CustomerServiceImpl` edit 缺 version 分支误用 `CODE_CUST_INVALID`(67001);`CODE_CAS_FAIL=67005` 已存在且 API-SUMMARY §2.2 已按 67005 登记。 - **位置**:`crm-customer/.../service/impl/CustomerServiceImpl.java` L145-148。 - **diff 摘要**:仅替换常量为 `CODE_CAS_FAIL`,消息文本「缺少乐观锁版本号」不变。 - **验证**:core F-05 分支断言 67005 转 ✅;携带正确 version 的 CAS 正路径不变;demo 与前端无按 67001 硬编码分流该场景(grep 已核),无需改文档。 --- ## 三、E2E 三套件前后对照 | 套件 | r2 基线(修复前) | 本 effort 终局 | delta | |---|---|---|---| | core(112 例) | ✅109 ⚠3 ❌0 | **✅111 ⚠1 ❌0** | D-02(F-08)/ D-03(F-05)转 ✅;⚠ 仅剩 F-01 stage=2 数据态(非缺陷) | | heavy(58→61 项*) | ✅52 ⚠4 ❌2 | **✅60 ⚠0 ❌1** | D-07(13.10)/ D-05(14.11/12b/13)全转 ✅;14.12 明细 8 行;❌1=MAIN teardown 瞬时远程库断连(pymysql 2013/10054,重试自愈,非断言) | | incr(63 例) | ✅60 ⚠2 ❌1 | **✅61 ⚠2 ❌0** | I-04 空 ids 分支补 40001 断言后转 ✅;⚠=I-01 取证型 + I-06 pool·board 漂移(均非缺陷) | | **合计** | 233 例 ✅221 ⚠9 ❌3 | **236 项 ✅232 ⚠3 ❌1** | ❌ 归零(断言口径);⚠ 9→3 全为任务书预期非缺陷类 | \* heavy 项数 +3 为本 effort 用例侧增补(14.12b 新增执行与断言、13.10 断言激活、F-15 三处收窄回改细分),非契约变更。 **本轮 heavy 五跑历程**(修正过程留档,证据在 `.scratch/customer-defectfix/e2e-heavy-fix*.log`): 一跑暴露 relation_star_level 同族缺陷(→补代码修复+重建);二跑暴露 14.11/12b 预期从未真正执行过的写死值(→DB 实证修正);三跑暴露 14.13 `0 or -1` Python falsy 陷阱(→`_is0` 字符串比较);四跑被会话压缩中断(F-13/F-14 已全 ✅);五跑终局 ✅60 ❌1。 --- ## 四、单测 - 命令:`mvn -pl crm-customer -am test`(Java 17 corretto) - 终局:**Tests run: 160, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS**(2026-09-06 10:13,`mvn-test-final.log`);-am 链 crm-base/file/auth/dict/rule/preference 全部通过 - 变更测试类:`CustomerImportIntegrationTest`(19 例)—— - 新增 D-04 用例:`doThrow` 600 字符异常 → markFailed 落库成功且 `CHAR_LENGTH ≤500`(连带修正 Mockito 重 stub 探针坑:`when(...).thenThrow` 会先命中 setUp 的旧 answer,必须 `doThrow(...).when(mock).method(...)`,并补 `setFileFileId("9001")`) - D-05 断言:confirm 后 insert 产物 `isBizNegotiated=0 / isChild=0 / relationStarLevel=0` - 说明:H2 测试库上述列可空,单测不暴露真实库 NOT NULL 炸——真实库行为由 E2E+手工复核兜住(本轮实证)。 --- ## 五、手工复核(任务书 §三.5) 脚本:`.scratch/customer-defectfix/manual-import-insert-check.py`(证据 `manual-import-result.json`) - 模板 `GET /api/customer/import/template`:HTTP 200,RFC5987 文件名 + xlsx 魔数 ✅ - 首轮(INSERT 行缺省/市/行业编码):任务终态 **DONE**(修复前必 FAILED)、2 行失败明细保留、reason 恰 500 字符(D-04 clip500 实证)——同时独立复证移交项「Analyzer 预检缺类型/省/市/行业必填校验」(§九.2) - 二轮(补全编号/省/市/行业,对齐 heavy F-14 r1 形态):**ALL_OK**—— - preview `totalCount=2 insertCount=2` - confirm → DONE `insertCount=2 failCount=0` - DB:2 行落库 `is_biz_negotiated=0 / is_child=0 / customer_stage=1 / owner=admin`、无失败明细 - 清理:2 条 defix 样例已随残留清理归档(§七)。 --- ## 六、副作用回收(共享库纪律) | 项 | 处置 | 结果 | |---|---|---| | collab 协同样例(id=750844482391375872)owner 被 heavy F-13 圈定刷成罗伟健/广东保伦 | 按票 04 实录定向 UPDATE 四字段(`restore-collab-owner.py`,单行 WHERE id=) | ✅ 还原 曾偲青(744842318024015872)/职员(744841292483133440);archive_status=1(有效)保持 | | e2c-销售甲 部门(13.10 临时挂入总监子树) | 用例内断言后立即还原 | ✅ 复核=华南销售部 seed(744700334353416192) | | F-15 提醒规则测试态 | heavy 15.11 自动还原出厂值 | ✅ 30/7 复查通过 | | 8080 服务 | heavy 脚本自带还原(默认 cron) | 报告收尾后停服释放(§十) | | 测试残留客户 | `clean-residue-defix.py` archive 正门 | ✅ 5 条非 seed 有效 e2c-* 全部归档,复扫非 seed 活跃 e2c-*=0(e2c-* 总 38:seed 17 + 已归档 21) | > 注意:`archive_status` 枚举为 **1=有效 / 2=已归档**(`Customer.java` L135-137);首轮清理脚本误用 `=0` 判活造成假阴性,已修正口径并复扫,此坑记入移交(§九.6)。 --- ## 七、变更文件清单 **产品代码(5)** 1. `crm-customer/src/main/java/com/crm/customer/task/CustomerImportExecutor.java` —— D-05(L247-252 缺省三连)+ D-04(L299-319 走 clip500、L321-327 新增 clip500) 2. `crm-customer/src/main/java/com/crm/customer/service/impl/CustomerTransferServiceImpl.java` —— D-07(L109-138,L121-124 批量部门名) 3. `crm-auth/src/main/java/com/crm/auth/service/impl/SysDeptServiceImpl.java` —— D-07(L119-137 ancestors 前缀查询) 4. `crm-customer/src/main/java/com/crm/customer/service/impl/CustomerFollowServiceImpl.java` —— D-02(L62-66) 5. `crm-customer/src/main/java/com/crm/customer/service/impl/CustomerServiceImpl.java` —— D-03(L145-148) **测试(1 改)** 6. `crm-customer/src/test/java/com/crm/customer/service/impl/CustomerImportIntegrationTest.java` —— D-04 新增 600 字符截断用例 + D-05 三断言 + Mockito/newTask 修正 **迁移 SQL 草案(1 新,待批未执行)** 7. `deploy/migration-d05-is-biz-negotiated-default.sql` —— is_biz_negotiated 补 DEFAULT 0(单事务 MODIFY)+ relation_star_level 放开 NOT NULL 注释块草案 + 回滚说明 **E2E 用例侧(2 改,仅断言/分支修正,不改契约)** 8. `.scratch/customer-e2e/e2e-heavy-r2.py` —— 13.10 seed 顶层树冲突处理(临时挂入/还原)+ 14.8 存 task_id2 + 14.11 预期 DB 实证修正(i=2 f=7 s=1,客户+联系人行都计数)+ 14.12b 新增(confirm UPDATE_ONLY 任务、u=2 执行期重新匹配语义)+ 14.13 真断言(r1 落库 biz=0 rel=0 / r2 星级=4,`_is0` 修 falsy 陷阱) 9. `.scratch/customer-e2e/e2e-incr.py` —— I-04 空 ids 分支补 40001(67001/400/40001 容忍集不变) **诊断/验证产物(本目录,非交付代码)**:`probe-assignable.py`(D-07 before/after)、`manual-import-insert-check.py`、`smoke-userlist-collab.py`、`restore-collab-owner.py`、`clean-residue-defix.py`、`stats-heavy5.py`、`stats-incr.py`、`diag-*.py`、各日志与 JSON 证据。 **文档**:API-SUMMARY.md 零改动(D-03 的 67005 已在 §2.2 登记;其余修复不改契约)。 --- ## 八、建议提交信息(不执行) ``` fix(customer): D-05 导入 INSERT 行补 is_biz_negotiated/is_child/relation_star_level 缺省赋值 fix(customer): D-04 导入失败原因截断防 varchar(500) 溢出二次异常 fix(customer): D-02 跟进 nextFollowTime 早于当前补 67009 拦截 fix(customer): D-03 编辑缺 version 错误码对齐 spec 改 67005 perf(customer,auth): D-07 assignable 去除逐用户部门名 N+1 并收窄子树查询 test(customer): 导入集成测试补 D-05 缺省列断言与 D-04 600 字符截断用例 chore(e2e): heavy/incr 用例侧修正——13.10 seed 树挂入还原、F-14 计数语义与 falsy 陷阱、I-04 40001 分支 chore(deploy): D-05 迁移 SQL 草案(is_biz_negotiated 补默认值,待批) ``` --- ## 九、未尽事项与风险移交 1. **共享库 DDL 待批**:`deploy/migration-d05-is-biz-negotiated-default.sql` 未执行(多人共用库纪律)。批准后执行可让 INSERT 路径不依赖应用层缺省;建议同批评估 `relation_star_level` 放开 NOT NULL(现 0=「未评估」占位,编辑时 requireStar 拦 0 强制重选 1~5,可自愈)。 2. **导入预检必填校验缺口(既有行为,非本次引入)**:`CustomerImportAnalyzer` 只校验名称必填+星级 1~5;类型/省/市/行业缺省的行 preview 计入 insert,执行期才行级失败(本次手工复核首轮实证 `Field 'city_code'`)。建议另起票把 NOT NULL 无默认列群的必填校验前移到预检,使 preview 计数与执行结果一致。 3. **heavy F-13 副作用沉淀(票 04 已知,本轮三度实证)**:交割圈定 BUDDY 名下全量且 teardown 不还原 collab owner——本轮已按票 04 实录还原(曾偲青/职员)。后续任何 heavy 复跑前后必须核对该样例四字段。 4. **残余 ⚠ 清单(3,全非缺陷)**:core F-01 stage=2 数据态(共享库演化);incr I-01 oplog 等级文案(取证型断言);I-06 pool·board 契约漂移(记档,归 Bruno/API-SUMMARY 文档侧处置,票 06 聚合)。 5. **heavy 终局唯一 ❌**:MAIN teardown 瞬时远程库断连(pymysql 2013 / WinError 10054),重试自愈、还原动作完整执行,非断言非缺陷。如需绝对零 ❌ 可重跑全套,但会再次触发 §九.3 副作用链,不建议为 cosmetic 指标重跑。 6. **口径坑记录**:`archive_status` 1=有效/2=已归档(无 0 值)——残留扫描判活谓词必须用 `=1`;`PageResult.total` 序列化为字符串;`py -c` 内联脚本在 PowerShell 下引号易碎,DB 取证一律落 `.py` 文件执行。 7. **前端/demo**:无需适配。D-02 的 67009 为新增分支(demo 现流程传未来时间);D-03 的 67005 已是文档登记值且无硬编码分流。 --- ## 十、收尾状态 - 233→236 项三套件复跑完成(断言口径 ❌=0);单测 160 例全绿;手工复核 ALL_OK;残留归零;副作用全部回收。 - 8080 服务已停(报告收尾动作),共享库无 drop/truncate、无无 WHERE 的 UPDATE。 - git 未做任何 commit/push,全部变更留待用户审查(提交信息见 §八)。