# 联系人族 + 重流程 + 规则族 — 字段逐入口对齐表(票 07 / customer-demo-ref) > 用途:客户单文件 Demo 冒烟断言 + 人工验收双用。字段名逐字来自 `.bru` / DTO·Param 源码,不许凭记忆编。 > 契约权威优先级:**Bruno `.bru` 出参示例 > DTO/Param 源码 > API-SUMMARY(下称 AS)**;请求绑定方式以控制器源码 + AS §0 为准(两处 `.bru` 请求体漂移已标注)。 > Bruno 仓根 `D:\code\crm-api-docs\`(下引 `.bru` 相对该根);后端源码根 `crm-customer/src/main/java/com/crm/customer/`(下称 `[DTO:x]`=`domain/dto/x.java`、`[PARAM:x]`=`domain/param/x.java`);crm-rule 源 = `crm-rule/src/main/java/com/crm/rule/`。 > 覆盖范围:覆盖矩阵「缺口动作≠豁免」中联系人族(#31-39、#50-52)+ 重流程(#55-63)+ 规则族(#66-69)。 ## 0. 阅读口径(全表通用) - **envelope**:所有 JSON 端点统一 `{code, success, message, data}`(`Result`,`code=0` 成功);下表「出参」一律指 **data 内** 字段,不再重复信封。二进制流端点(模板/导出)**不走 envelope**。 - **空值口径**:Demo 渲染空值(null/空串/缺失字段)一律显 `--`;雪花 Long id JSON 序列化为**字符串**,前端禁 Number 运算。 - **脱敏口径**:联系人电话在**全部读路径**服务端脱敏(`138****1234`,源 `ContactPhoneMasker`);明细见文末「脱敏专节」。 - **分页**:入参继承 `BaseParam`(`current=1`/`size=10`(≤500)/`keyword`/`orderBy`/`asc=false`,源 `crm-base/.../param/BaseParam.java`);出参 `PageResult` = `{content[], total, size, current, pages, empty}`(total/size/current/pages 实测序列化为字符串)。 - **标注缩写**:G5=联系人负责人列缺(P2 记档);D-G1=batchEdit 空串清空不可达(P2 豁免);D26=电话软提示受查重开关控制;D22=resign 自动推导接收总监;D23=全分配自动置已完成;D-07=assignable 超时(已修复);P1-1=导入 INSERT 必填前移(已修复);POOL-CFG=公海池只读占位卡。 - **seed 示例值**:admin userId=`739564171091247104`;客户 `750844477165273088`(e2c-全字段-科技,KH202609030010);联系人 `750844500171030528`(张关键)/`750844501387378688`(王重复,同号 13812340001 重复样例);交接单号形态 `JG202609030001`。 --- ## 一、联系人独立菜单(TREE A4-5) ### #50 顶级联系人列表 — `GET /api/customer/contact/page` - 出处:`A4 客户管理\联系人\顶级联系人列表.bru` + `[PARAM:ContactPageParam]`(extends BaseParam,**无自有字段**,keyword 匹配姓名/电话/所属客户名,客户可见性收口)+ `[DTO:ContactDTO]` - 请求参数: | 名 | 类型 | 必填 | 示例 | |---|---|---|---| | current | number | 否 | 1 | | size | number | 否 | 10 | | keyword | string | 否 | 恒信达 | | orderBy / asc | string/boolean | 否 | createTime / false | - 出参(data = PageResult,content[] 元素): | 字段路径 | 类型 | 口径/文案 | 脱敏 | |---|---|---|---| | content[].id | Long(字符串) | 联系人主键 | -- | | content[].customerId | Long(字符串) | 所属客户 id(新增必填;编辑以库内归属为准不可换客户) | -- | | content[].name | String | 联系人姓名 | -- | | content[].jobTitleName | String | 职务名称(文本,必填;自定义职务时下面三列空) | -- | | content[].jobTitleId | Long(字符串) | 标准职务 id(可空) | -- | | content[].jobTitleCategory | String | 职务类别(高层管理/采购/…,可空) | -- | | content[].jobTitleLevel | Integer | 职务层级 1–10;null=未设置等级落右侧栏 | -- | | content[].phone | String | 联系电话 | **是**(前3+****+后4) | | content[].isKeyContact | Integer | 是否关键联系人(是1/否0) | -- | | content[].isInternal | Integer | 是否内线(是1/否0) | -- | | content[].giftRemark | String | 礼品备注 | -- | | content[].source | String | 来源字典 contact_source:组会/组局/转介绍/其他 | -- | | content[].customerName / customerNo | String | 所属客户名称/编号(仅出参) | -- | | total/size/current/pages/empty | — | 分页元信息 | -- | - 备注: - **`.bru` 示例漂移**:示例里 `phone:"13812340001"`、`source:"contact_source_01"` 均为票 04 脱敏/字典改版**前的陈旧真值**;现行源码 `CustomerContactServiceImpl.page` 对每行 `ContactPhoneMasker.mask(...)`(service L150),权威 source 值域见字典节(`group_meeting` 等)。 - 新增入口:Demo 独立页签 + `scopeKey=contact.list` 列偏好(`A4 客户管理\联系人\读取列偏好.bru`/保存列偏好.bru)与形态偏好(读取/保存视图形态.bru)接线。 ### #51 联系人详情 — `GET /api/customer/contact/detail?id=` - 出处:`A4 客户管理\联系人\联系人详情.bru` + `[DTO:ContactDTO]` - 请求参数:`id`(string,必填,联系人 id,查询参数) - 出参(data):与 #50 列表行**同一 ContactDTO 字段集**(id/customerId/name/jobTitleName/jobTitleId/jobTitleCategory/jobTitleLevel/phone/isKeyContact/isInternal/giftRemark/source + customerName/customerNo),示例另带 createTime/updateTime。 - 备注: - `.bru` 实测示例 `phone:"138****0101"`(脱敏形态✅,与现行代码一致);错误码 67008 联系人不存在(AS §2.7 行记 67002,以 `.bru` 为准并注漂移)。 - 与列表行差异:无(同 DTO);Demo 旧代码编辑弹窗直接用行数据回显(L1143),新入口回显应走本端点(明细行级最新值)。 - phone 脱敏回显形态(含 `*`)提交回 edit/batchEdit = 用户未改号,服务端跳过 phone 更新(`CustomerContactServiceImpl.edit` L89)。 ### #52 联系人全局搜索 — `GET /api/customer/contact/search?keyword=&limit=` - 出处:`A4 客户管理\联系人\联系人全局搜索.bru` + `CustomerContactServiceImpl.search`(L155) - 请求参数: | 名 | 类型 | 必填 | 示例 | |---|---|---|---| | keyword | string | 是 | 恒信达 | | limit | number | 否 | 20(默认 20;≤0 回落默认) | - 出参(data = 数组):元素字段与 #50 列表行完全一致(ContactDTO + customerName/customerNo);**phone 服务端脱敏**(`.bru` 示例明文=陈旧漂移,同 #50)。 - 备注:商机「商机关键联系人」数据源(姓名/电话模糊 + 带出所属公司名);客户可见性收口;Demo 新增入口,演示商机选联系人场景。 --- ## 二、联系人列表(详情页签数据源) ### #31 客户联系人列表 — `GET /api/customer/contacts?customerId=` - 出处:`A3 商机管理\销售机会\按客户查联系人列表.bru`(A4 侧无独立 `.bru`,商机字段 17 级联数据源兼详情页签 2;AS §2.5 页签2 / §2.4)+ `[DTO:ContactDTO]`(`CustomerContactServiceImpl.listByCustomer` L123) - 请求参数: | 名 | 类型 | 必填 | 示例 | |---|---|---|---| | customerId | string | 是 | 750844477165273088 | - 出参(data = 数组,ContactDTO 全字段):`id / createTime / updateTime / customerId / name / jobTitleName / jobTitleId / jobTitleCategory / jobTitleLevel / phone / isKeyContact / isInternal / giftRemark / source / customerName / customerNo`(口径同 #50 表)。 - 备注: - **8 列渲染**(Demo renderContactsList L844):姓名/职务/电话/关键联系人/内线/来源/礼品备注(+勾选/序号/操作列);`isKeyContact/isInternal` 1→是 0→否;空文本显 `--`。 - **无负责人列 = G5 标注**:ContactDTO 不含 owner 字段(DTO 全字段核对),G5 挂 07 号票「现状值域+定稿口径」两栏。 - **电话脱敏形态**:`.bru` 示例 `13812340002` 明文 = 票 04 前**陈旧真值**;现行代码 listByCustomer 逐行 `ContactPhoneMasker.mask(...)`(L131)→ 断言按 `138****0002` 形态。 - 权限继承所属客户(无独立 ACL,无权 67002);错误码 67002。 --- ## 三、联系人图谱 ### #32a 图谱全量读 — `GET /api/customer/contact/graph/detail?customerId=` - 出处:`A4 客户管理\客户详情\联系人图谱\图谱全量读.bru` + `[DTO:GraphDTO/GraphNodeDTO/GraphEdgeDTO]` - 请求参数:`customerId`(string,必填,查询参数) - 出参(data): | 字段路径 | 类型 | 口径/文案 | 脱敏 | |---|---|---|---| | version | Integer | 图谱版本号(save 乐观锁比对口径;联系人增删等节点集变化也会 bump;首次读懒建版本行) | -- | | nodes[].contactId | Long(字符串) | 联系人 id | -- | | nodes[].name | String | 联系人姓名 | -- | | nodes[].jobTitleName | String | 职务名称 | -- | | nodes[].jobTitleCategory | String | 职务类别(一级类别文本) | -- | | nodes[].level | Integer | 等级 1–10(10=顶层);null=未设置等级(右侧栏) | -- | | nodes[].levelSource | Integer | 等级来源:1=字典跟随 2=手工锁定(D2) | -- | | nodes[].phoneMasked | String | 联系电话(**字段名即脱敏承诺**) | **是** `138****9901` | | nodes[].isKeyContact | Integer | 是否关键联系人(1/0) | -- | | nodes[].isInternal | Integer | 是否内线(1/0,悬停气泡展示) | -- | | nodes[].giftRemark | String | 礼品备注(悬停气泡,可空) | -- | | edges[].parentId / childId | Long(字符串) | 手动边:上级/下级联系人 id(level 高者→低者) | -- | - 备注:**公司边派生注**——nodes 只含客户联系人,公司节点/公司边不在数据里,由前端按 `level==10` 派生渲染(D8,修订⑧口径:只有手动边,无自动生成/否决);edges 只含手动边全量终态;布局纯由 level 推导(y=等级行、x=行内稳定序),零坐标数据。错误码 67002。跨客户人脉/节点展开折叠=豁免(原型本期暂不支持)。 ### #32b 图谱整体保存 — `POST /api/customer/contact/graph/save` - 出处:`A4 客户管理\客户详情\联系人图谱\图谱整体保存.bru` + `[PARAM:GraphSaveParam]` - 请求参数(表单绑定,嵌套行走 indexed): | 名 | 类型 | 必填 | 示例 | |---|---|---|---| | customerId | string | 是 | 753314901191032832 | | version | number | 是 | 1(读端点返回值原样带回;不符→67018) | | nodes[i].contactId | string | 是 | (必须属于 customerId 客户) | | nodes[i].level | number | 否 | 1–10;null=撤级(撤级须无任何边) | | edges[i].parentId / childId | string | 是 | 上级/下级联系人 id | - 出参(data):`Integer` 新版本号(实测示例 `2`)。 - 备注:**整体保存语义**——全量快照单事务:版本 CAS(67018 冲突)→ 边校验(67019 自连/重复/等级倒挂或越界/未定级端点/端点越界;67020 单父违反/等级调整约束)→ level 批写 + 边全量替换(edges 空=清空全部边);D4 编辑态变更驻前端 pending,save 一次落库。错误码 67002/67018/67019/67020。 --- ## 四、联系人列表侧增强 ### #33 页内批量编辑 — `POST /api/customer/contact/batchEdit` - 出处:`A4 客户管理\客户详情\联系人\页内批量编辑.bru` + `[PARAM:ContactBatchEditParam]` + `[DTO:ContactBatchResultDTO]` - 请求参数(表单绑定,`rows[0].xxx` indexed): | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | rows[i].id | string | 是 | 联系人 id | | rows[i].name | string | 否 | null=不改;提交则须非空 | | rows[i].jobTitleCode | string | 否 | 标准职务字典 code;null=不改;变更走 D2 联动重算 level | | rows[i].phone | string | 否 | null=不改;**脱敏形态(含*)=不改**(回显原样提交);明文=修改 | | rows[i].source | string | 否 | 来源字典 code;null=不改 | | rows[i].isKeyContact | number | 否 | null=不改 | - 出参(data = ContactBatchResultDTO):`successCount:int`(原子批:全过=N,任一失败=0)、`failedRows[]`:`rowIndex:int`(提交数组下标从 0 计)+ `reason:String`。 - 备注: - **空串提交=不修改(D-G1 标注)**:Param.phone 注释声称「空串=清空」,但 Spring 表单绑定(StringEditor allowEmpty)把空串转 null=不改,「空串=清空」**不可达**——D-G1(P2 语义缺口)记档豁免,Demo 按「绑定语义空串→null=不修改」断言。 - 原子批:任一行校验失败零落库并返回失败明细(前端定位首个错误行);「只改当前页」由前端提交集合表达,后端无页语义;职务变更 + 任一行改成功 bump 图谱版本。错误码 67008/67022(整批不可用=行集空)。 ### #34 电话明文查看 — `GET /api/customer/contact/reveal?id=` - 出处:`A4 客户管理\客户详情\联系人\电话明文查看.bru` - 请求参数:`id`(string,必填,联系人 id) - 出参(data):`String` **明文手机号**(实测示例 `13800001003`)——reveal 是唯一明文通道。 - 备注:行为埋点落 `customer_contact_reveal_log`(谁/何时/哪个联系人/IP,失败自吞不阻塞);错误码 67008。Demo 侧**列表与图谱共享 `revealed` 态**(`revealed[id]=true` 双写列表行与图谱节点,index.html L609/L1848-1864),「明文查看」按钮=全量逐个 reveal。 ### #35 联系人导出 — `POST /api/customer/contact/export?customerId=` - 出处:`A4 客户管理\客户详情\联系人\联系人导出.bru` + `[DTO:ContactExportRow]`(domain/excel) - 请求参数:`customerId`(string,必填,查询参数) - 出参:**HTTP 200 xlsx 二进制流**(PK 头 `50 4b 03 04`,不走 JSON 信封,前端按附件流下载);服务端 MinIO 留档 90 天。 - 备注:**导出电话脱敏口径同列表——已从源码确认**(`CustomerContactEnhanceServiceImpl` L307 `row.setPhone(ContactPhoneMasker.mask(...)) // 脱敏口径不因导出放开`;`.bru` docs 同句)。xlsx 列集(ContactExportRow 7 列):姓名 / 职务 / 职务类别 / 职务等级 / **手机号(脱敏)** / 来源 / 关键联系人(1=是)。 --- ## 五、联系人导入六端点(#36,镜像客户导入) 端点×语义总表(出处:`A4 客户管理\客户详情\联系人导入\*.bru` 六件 + `CustomerContactImportController` /api/customer/contact/import): | 端点 | 方法 | 语义 | 关键错误码 | |---|---|---|---| | /template | GET | 模板下载:**单 sheet 纯表头五列**(姓名 / 职务(字典编码) / 手机号 / 来源(字典编码) / 是否关键联系人(是/否),源 `ContactImportTemplateRow`;表头即版本锚点,无示例行;RFC 5987 中文文件名),xlsx 流不走信封 | — | | /upload | POST(multipart) | 上传+预校验(见下),任务落待确认态,返回预计数+明细 | 67023 | | /confirm?taskId= | POST | 待确认→处理中+异步执行(行级部分成功);重复确认 67024 | 67024 | | /result?taskId= | GET | 任务结果(状态+六计数+提交人/时间) | 67024 | | /failures?taskId= | GET | 失败/疑似重复明细(疑似重复行以「疑似重复」前缀区分) | 67024 | | /page | GET | 最近导入记录(仅当前用户任务,状态/方式过滤,时间倒序) | — | **/upload 请求参数**(multipart,源控制器 L49 @RequestParam 逐字): | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | customerId | string | 是 | 导入目标客户 id(固定,不可切换) | | file | file | 是 | .xls/.xlsx,≤20MB,单 sheet ≤5 万行,模板版本校验 | | importMode | string | 是 | APPEND_ONLY 仅新增 / UPDATE_ONLY 仅更新 / UPSERT 新增或更新 | | duplicateStrategy | string | 否 | SKIP 不导入(默认)/ OVERWRITE 覆盖导入 | **关键 DTO 字段**: - `ContactImportPreviewDTO`(upload 出参):`taskId / customerId / importMode / duplicateStrategy / totalCount(数据行总数)/ insertCount(预计新增)/ updateCount(预计更新,含未变化)/ failCount(预校验失败)/ suspectCount(疑似重复,不写入人工处理)/ rows[](预校验明细,**上限 100 条**)`;rows[] 行:`rowNum`(数据行号从 1 起)/ `identity`(联系人识别=姓名原值)/ `verdict`(**INSERT/UPDATE/FAIL/SUSPECT**)/ `reason`(失败/疑似原因,可空)。**预检计数=insert/update/fail/suspect 四计数齐全**(源码逐字核对)。 - `ContactImportResultDTO`(result/page 行出参):`taskId / customerId / importMode / templateVersion(V1)/ status(**0待确认 1处理中 2已完成 3已失败**)/ totalCount / insertCount / updateCount / unchangedCount(未变化,UPDATE 行值相同时)/ failCount / suspectCount / failReason(已失败时)/ creatorName / createTime / finishTime`。 - `ContactImportFailDTO`(failures 出参行):`rowNum / name(姓名原值,行静态缺失时为空)/ phone(手机号原值,可空)/ failReason`;**无 sheet 维度**(单 sheet 导入,对称 ImportFailDTO 有 sheetName 的差异点)。 - `ContactImportPageParam`(page 入参):BaseParam + `status`(0-3)+ `importMode`(三值)。 - 备注:行级判定与执行同口径(必填/字典值域/客户内手机号查重);有手机号按手机号查重;任一行成功 bump 图谱版本;Demo cimp 三步弹窗已全覆盖六端点(L2131-2186)。 --- ## 六、快速添加 / CRUD / 电话软提示 ### #37 快速添加联系人(多行)— `POST /api/customer/contact/quickAdd` - 出处:`A4 客户管理\客户详情\联系人\快速添加联系人.bru` + `[PARAM:ContactQuickAddParam]` - 请求参数(表单绑定,`rows[0].xxx` indexed): | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | customerId | string | 是 | 目标客户 id(固定,不可切换) | | rows[i].name | string | 是 | 联系人姓名 | | rows[i].jobTitleCode | string | 是 | job_title 组二级项字典 code(联动补全 id/名称/类别/档位,D1 初始值 level_source=1) | | rows[i].phone | string | 否 | 可空;非空须 11 位手机号格式 | | rows[i].source | string | 否 | contact_source 组字典 code,可空 | | rows[i].isKeyContact | number | 否 | 是1/否0,空=0 | - 出参(data = ContactBatchResultDTO):`successCount:int` + `failedRows[]{rowIndex:int(提交数组下标从 0 计), reason:String}`。 - 备注:**部分成功**(合法行落库、非法行明细返回;空行跳过;失败行留在编辑器改完只重提失败行不重复创建);有手机号按手机号查重、无手机号按客户+姓名+职务疑似重复提示不静默创建;任一行成功 bump 图谱版本;错误码 67021(行集空=整批不可用)/67008。 ### #38 联系人新建 / 编辑 / 删除 — `POST contact/create`、`POST contact/edit?id=`、`POST contact/delete?id=` - 出处:`A4 客户管理\联系人\新增联系人.bru` + `编辑联系人.bru` + `删除联系人.bru` + `[DTO:ContactDTO]`(写入参/出参双向,ADR-0017 Route A) - 请求参数(表单绑定): | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | customerId | string | create 必填 | 所属客户 id;编辑以库内归属为准不可换(入参忽略) | | name | string | 是 | 联系人姓名 | | jobTitleName | string | 是 | 职务名称(自由文本;标准职务另传 jobTitleId 三列) | | jobTitleId / jobTitleCategory / jobTitleLevel | string/string/number | 否 | 标准职务 id / 类别 / 层级 1–10 | | phone | string | 否 | 手机号格式;**脱敏回显形态(含*)提交=不改号保留库内原值**(edit,L89) | | isKeyContact / isInternal | number | 否 | 是1/否0 | | giftRemark | string | 否 | 礼品备注 | | source | string | 否 | contact_source 字典 code | | id(查询参数) | string | edit/delete 必填 | 联系人 id | - 出参(data):create = `Long` 新联系人 id(字符串);edit/delete = `null`。 - 备注: - **扩展档案字段(性别/生日/微信/邮件/地址)V1 收窄不在 ContactDTO**——豁免标注(GR 豁免台账),DTO 逐字段核对无此类字段,Demo 不得渲染这些输入位。 - 权限继承所属客户(无独立 ACL),无权 67008/67002;edit 错误 67008,delete 软删(可能被商机/项目引用做快照,级联清图谱边 + bump 版本)。 - 写动作记 oplog `CONTACT_ADD/CONTACT_EDIT/CONTACT_DELETE`(F10)。 - Demo 旧代码 dlgContact(L1142-1159)直接行数据回显 + 硬编码 source 旧 code(见字典节漂移)。 ### #39 电话软提示 — `GET /api/customer/contact/check-phone?phone=&excludeId=` - 出处:`A4 客户管理\联系人\电话软提示.bru` + `[DTO:ContactPhoneHitDTO]`(service L171) - 请求参数: | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | phone | string | 是 | 联系人手机号 | | excludeId | string | 否 | 排除的联系人 id(编辑页传自身) | - 出参(data = 数组,命中行): | 字段路径 | 类型 | 口径/文案 | 脱敏 | |---|---|---|---| | [].contactId | Long(字符串) | 联系人 id(masked 时 null) | masked 时 -- | | [].contactName | String | 联系人姓名(masked 时 null) | masked 时 -- | | [].jobTitleName | String | 职务名称(masked 时 null) | masked 时 -- | | [].customerId | Long(字符串) | 所属客户 id(masked 时 null) | masked 时 -- | | [].customerName | String | 所属客户名称(masked 时 null) | masked 时 -- | | [].masked | Boolean | 命中行对当前用户是否脱敏(所属客户不可见=只提示「存在匹配记录」) | -- | - 备注: - **D26 注**:跨客户电话命中只提示疑似重复、**不阻断**;受查重设置「联系电话」开关(`phoneEnabled`,crm-rule 单例)控制——`settings.phoneSoftCheckEnabled()` 为假时恒返回空数组(单例未落地=出厂默认电话软提示开)。 - 序列化漂移:源码 `ownerUserId/ownerDeptId` 标 `@Schema(hidden=true)`(服务层判权用),但 hidden 只影响 swagger 文档**不影响 JSON 序列化**——`.bru` 实测示例确带这两个字段;前端不应依赖,Demo 断言只按上表 6 字段。 --- ## 七、客户交割(#55-59,TREE A4-7) > **级联语义注(全组)**:发起交割**整批原子**(@Transactional)——圈定=当前用户名下可交接客户(唯一负责人+有效+非归档+非公海),**initiate 无 customerIds 入参,圈定=preview 全量**;名下客户 owner→接收总监 + **同步商机/项目 owner**(port 联动),任一失败全部回滚不生成交接单;oplog TRANSFER(系统操作,含交接编号)。交接编号形态 **`JG+yyyyMMdd+4位序`**(实测 `JG202609110001`)。 ### #55 发起前预览 + 发起交割 — `GET transfer/preview` + `POST transfer/initiate` - 出处:`A4 客户管理\客户交割\发起前客户预览.bru` + `发起交接.bru` + `[DTO:TransferPreviewDTO/TransferInitiateDTO]`(initiate 返回 `TransferDetailDTO`,控制器 L48) - preview 请求参数:**无业务参数**(当前用户上下文圈定)。 - preview 出参(data = 数组,TransferPreviewDTO): | 字段路径 | 类型 | 口径/文案 | |---|---|---| | [].customerId | Long(字符串) | 客户ID | | [].customerName | String | 客户名称 | | [].customerNo | String | 客户编号(KH 形态) | | [].customerStarLevel | Integer | 客户星级 1~5 | | [].customerStage | Integer | 客户阶段:**1潜在 2重潜 3已成交** | - initiate 请求参数(表单绑定 TransferInitiateDTO): | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | reason | string | 是 | transfer_reason 字典 code:**resign 员工离职 / transfer_post 岗位调动 / region_adjust 区域调整** | | toDirectorId | string | 否 | 接收总监用户 id;**resign 自动推导不传(D22)**;调动/区域调整必传且校验在直属总监范围内(67011) | | remark | string | 否 | 备注(选填) | - initiate 出参(data = TransferDetailDTO,字段同 #57 详情表)。 - 备注:错误码 67010(存在未完成交接单,防重入)/67011(无可交接客户/接收总监不合法/原因缺失)/67012。 ### #56 交接单列表 — `GET transfer/page` - 出处:`A4 客户管理\客户交割\交接记录分页.bru` + `[PARAM:TransferPageParam]`(javadoc:keyword 匹配**发起人姓名**;时间倒序) - 请求参数: | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | transferNo | string | 否 | 交接编号(模糊,JG* 形态) | | reason | string | 否 | transfer_reason code(精确) | | status | number | 否 | **0 待分配 1 已完成**(`.bru` 示例查询串填 status=2 超值域,以文档表+源码注释 0/1 为准) | | current/size/keyword/orderBy/asc | — | 否 | BaseParam | - 出参(data = PageResult,content[] 元素 = TransferDetailDTO):`id / transferNo(JG+yyyyMMdd+4位序)/ fromUserName(发起人)/ toDirectorName(接收总监)/ reason(字典 code)/ remark / totalCount / assignedCount(进度=assigned/total)/ status(0/1)/ createTime / details[](明细行,同 #57)` + 分页元信息。 - 备注:列表行即完整单头(含明细嵌套),Demo 渲染进度条用 assigned/totalCount。 ### #57 交接单详情 — `GET transfer/detail?id=` - 出处:`A4 客户管理\客户交割\交接详情.bru` + `[DTO:TransferDetailDTO]` - 请求参数:`id`(string,必填,交接单 id)。 - 出参(data = TransferDetailDTO): | 字段路径 | 类型 | 口径/文案 | |---|---|---| | id | Long(字符串) | 交割单ID | | transferNo | String | 交接编号(JG+yyyyMMdd+4位序) | | fromUserName / toDirectorName | String | 发起人 / 接收总监姓名 | | reason | String | 交接原因字典 code(transfer_reason) | | remark | String | 备注 | | totalCount / assignedCount | Integer | 交接客户总数 / 已分配数(进度=assigned/total) | | status | Integer | **0待分配 1已完成** | | createTime | String | 发起时间(yyyy-MM-dd HH:mm:ss) | | details[].id | Long(字符串) | 明细ID | | details[].customerId | Long(字符串) | 客户ID | | details[].customerName | String | 客户名称(实时回显;**已删客户 null 前端兜底 `--`**,DTO 注释) | | details[].assignStatus | Integer | 明细**处置态值域:0待分配 1已分配** | | details[].assignUserName | String | 分配对象姓名(待分配 null→`--`) | | details[].assignTime | String | 分配时间(待分配 null) | - 备注:错误码 67002(不存在);单头+明细与 initiate 返回共用同一 DTO。 ### #58 明细分配 + 可分配销售 — `POST transfer/assign?id=&customerIds=&assignUserId=` + `GET transfer/assignable?id=` - 出处:`A4 客户管理\客户交割\客户分配.bru` + `可分配销售列表.bru` + `[DTO:AssignableUserDTO]` - assign 请求参数:`id`(交接单 id,必填)+ `customerIds`(客户 id 集合,**重复键** `customerIds=a&customerIds=b`,必填)+ `assignUserId`(目标销售用户 id,必填)——均查询参数。 - assign 出参(data = BatchResult):`total:int / successCount:int / failCount:int / failures[]:{id:Long(失败客户 id), reason:CustomerBatchFailReason(语义枚举), message:String(失败文案)}`。 - assignable 出参(data = 数组,AssignableUserDTO):`userId:Long(字符串) / userName:String(姓名)/ deptName:String(部门名)`——接收总监部门(含子部门)+ 启用在职,**排除发起人与总监本人**。 - 备注: - 分配语义:接收总监→具体销售;**逐条独立事务部分成功**,失败明细列原因(67012:已分配/越权/为发起人本人/为当前负责人);**全部分配完成自动置已完成(D23)**。 - **D-07 已修复注**(customer-defectfix 2026-09-06,AS §5.6/§4):assignable 曾稳定超时(P1)——根因两处全修(根部门全树递归改 ancestors 前缀查询、逐用户 getDeptNames N+1 改批量回显),6046 人实测 0.42s/0.40s(目标 <5s),heavy 13.10 E2E 断言绿;Demo 可正常拉取并断言 <5s。 ### #59 发起交割入口 —(列表批量面板「发起交割」按钮) - 无独立端点(跳转 dlgTransferEntry L1033 → 走 #55 预览/发起)。无缺口。 --- ## 八、客户导入六端点(#60-63,TREE A4-3-2) 端点×语义总表(出处:`A4 客户管理\我的客户\客户导入\*.bru` 六件 + `CustomerImportController` /api/customer/import): | 端点 | 方法 | 语义 | 关键错误码 | |---|---|---|---| | /template | GET | 模板下载:**V1 双 sheet 纯表头**(「客户」9 列:客户编号/客户名称/客户类型/统一社会信用代码/省份编码/城市编码/行业编码/客户星级(1-5)/备注,源 `CustomerImportServiceImpl.CUSTOMER_HEADERS`;「联系人」5 列:客户编号/联系人姓名/联系电话/职务名称/是否关键联系人(是/否),`CONTACT_HEADERS`;表头即版本锚点,无示例行),xlsx 流不走信封 | — | | /upload | POST(multipart) | 上传+预校验:文件守门(.xls/.xlsx、≤20MB、模板版本、单 sheet ≤5 万行)→ 行级判定(与执行同口径)→ 任务落待确认(DRAFT),返回预计数+明细 | 67013 | | /confirm?taskId= | POST | 待确认→处理中+异步执行(两段式);重复确认 67014 | 67014 | | /result?taskId= | GET | 任务结果(状态+六计数+提交人/时间) | 67002(任务不存在) | | /failures?taskId= | GET | 失败/疑似重复明细 | 67014 | | /page | GET | 任务分页(仅当前用户任务,状态/方式过滤,时间倒序) | — | **/upload 请求参数**(multipart,源控制器 L50 逐字): | 名 | 类型 | 必填 | 口径 | |---|---|---|---| | file | file | 是 | Excel 文件 | | importMode | string | 是 | APPEND_ONLY 仅新增 / UPDATE_ONLY 仅更新 / UPSERT 有则更新 | | duplicateStrategy | string | 否 | SKIP 不导入(**缺省**)/ OVERWRITE 覆盖导入(返工票 09-F8;非法值 67013 拦截) | **关键 DTO 字段**: - `ImportPreviewDTO`(upload 出参):`taskId / importMode / duplicateStrategy(响应回显)/ totalCount(数据行总数,**客户 sheet**)/ insertCount / updateCount(含未变化)/ failCount / suspectCount(疑似重复,不写入人工处理)/ contactCount(**联系人 sheet 行数,可选**)/ rows[](预校验明细,上限 100 条)`;rows[] 行:`sheetName(客户/联系人)/ rowNum(从 1 起)/ identity(客户识别=编号或名称原值)/ verdict(INSERT/UPDATE/FAIL/SUSPECT)/ reason`。与联系人导入 preview 的差异=**多 sheetName + contactCount、少 customerId**。 - `ImportResultDTO`(result/page 行出参):`taskId / importMode / templateVersion(V1)/ status(0待确认 1处理中 2已完成 3已失败,状态机 0 DRAFT→1 RUNNING→2 DONE/3 FAILED)/ totalCount / insertCount / updateCount / unchangedCount / failCount / suspectCount / failReason / creatorName / createTime / finishTime`(无 customerId——差异点同上)。 - `ImportFailDTO`(failures 出参行):`sheetName(客户/联系人)/ rowNum / customerIdentity(文件内编号或名称原值)/ failField(行级字段错误记列名,整行失败为空)/ failReason(疑似重复行以「疑似重复」前缀区分)`。 - `ImportPageParam`(page 入参):BaseParam + `status`(0-3)+ `importMode`(三值)。 - 备注: - **INSERT 行必填五列(customerType/provinceCode/cityCode/industryCode/customerStarLevel)预检即 FAIL「XX必填」= P1-1 已修复注**(handover 票 03 前移,AS §2.12;预计数=执行同口径);UPDATE 行部分更新不受限。 - **D-05 已修复注**:is_biz_negotiated NOT NULL 无默认值致 INSERT 必 FAILED——已修(INSERT 显式补缺省 isBizNegotiated=0/isChild=0/relationStarLevel=0)。 - **D-04 已修复注**:失败原因超长写不进明细——已修(行级/任务级失败原因统一截断 ≤500)。 - F8 `duplicateStrategy` 语义(D24 冻结):SKIP=文件内重复组全部标失败不写入;OVERWRITE=文件内自上而下**最后一条通过校验的行 UPDATE 生效**;作用面**仅文件内重复组**,不触名称相似 SUSPECT 判定。 - 统一社会信用代码列仅作匹配锚+新增写入,更新行不回改(CustomerImportRow javadoc)。 --- ## 九、规则族(#66-69,TREE A7 业务规则/客户规则) > 两单例均 crm-rule 跨模块服务于客户域:GET 读单行 / POST /save 全量覆盖保存即生效;单例 id 固定=1(`CustomerRuleConstants.SINGLETON_ID`,Initializer 首装种子幂等);审计字段不出报文;操作轨迹以应用日志记录(crm-rule 无 oplog 基建)。 ### #67a 查重设置读取 — `GET /api/rule/customer/dedup` - 出处:`A7 后台管理\业务规则\客户规则\客户管理设置(客户查重设置)\读取查重设置.bru` + crm-rule `domain/dto/CustomerDedupRuleDTO.java` + `controller/CustomerDedupRuleController.java` - 请求参数:无。 - 出参(data,5 业务字段 + 元信息): | 字段路径 | 类型 | 口径/文案 | |---|---|---| | id | string | 配置行 id(单例恒 "1") | | masterEnabled | number | 总开关:1启用 0禁用(仅控制名称/电话提示;**信用代码强制规则不受影响**) | | nameEnabled | number | 客户名称查重独立开关:1启用 0禁用 | | nameMatchMode | number | 名称匹配方式:**1精确 2模糊**(模糊时相似度阈值生效;出厂默认 2) | | similarityThreshold | number | 名称相似度阈值 **0~100**(模糊匹配生效;出厂标准 80;a7-3-3-2 §4.1 档位 70/80/90/自定义0-100) | | phoneEnabled | number | 联系电话查重独立开关:1启用 0禁用(精确命中仅提示不阻断 → 控制 #39 check-phone,D26) | | updateTime | string | 最近保存时间(回显只读) | ### #67b 查重设置保存 — `POST /api/rule/customer/dedup/save` - 出处:`A7 后台管理\业务规则\客户规则\客户管理设置(客户查重设置)\保存查重设置.bru` + 同上控制器/Service - 请求参数(表单绑定,ADR-0017):`masterEnabled / nameEnabled / phoneEnabled`(number,否,缺省=0 关闭)+ `nameMatchMode`(否,缺省→出厂 2 模糊;显式传入须 ∈{1,2})+ `similarityThreshold`(否,缺省→出厂 80;显式传入须 ∈[0,100])。 - 出参(data):保存后全量配置(同 #67a 结构,含 id/updateTime)。 - 备注: - 校验越界 **64023**(`CODE_CUSTOMER_RULE_INVALID`,service:匹配方式必须 1/2、阈值必须 0~100 整数;不落库)。 - **统一社会信用代码恒开无配置字段**(DTO 逐字段核对无 creditCode 项;前端固定渲染「精确匹配+不可关闭」;消费侧 L3 硬拦 67003)。 - 三预设对照:宽松 70 / 标准 80(出厂推荐)/ 严格 90,Demo 阈值控件建议三档+自定义。 - 消费侧:crm-customer `RuleBackedCustomerDedupSettingProvider`(@Primary)读单例驱动新增/编辑内建查重——总开关∧单项开关控名称/电话,阈值/方式透传(EXACT=trim+大小写不敏感全等;模糊=ngram);单例缺失回退出厂值;匹配方式消费边界=新增/编辑内建查重(**导入路径维持模糊**)。 ### #66a 超期提醒读取 — `GET /api/rule/customer/reminder` - 出处:`A7 后台管理\业务规则\客户规则\客户管理设置(超期未跟进提醒)\当前提醒规则.bru` + crm-rule `domain/dto/CustomerReminderRuleDTO.java` + `controller/CustomerReminderRuleController.java` - 请求参数:无。 - 出参(data): | 字段路径 | 类型 | 口径/文案 | |---|---|---| | id | string | 单例 id("1") | | masterEnabled | number | 总开关 1启用 0禁用 | | firstTriggerEnabled | number | 首次触发独立开关(**首触出厂 30 天**,`DEFAULT_FIRST_TRIGGER_DAYS=30`) | | firstTriggerDays | number | 首次触发天数(>0 正整数;锚点后 N 天无跟进落首次提醒) | | secondIntervalEnabled | number | 二次提醒独立开关(**间隔出厂 7 天**,`DEFAULT_SECOND_INTERVAL_DAYS=7`) | | secondIntervalDays | number | 二次提醒间隔天数(>0 正整数;首次后每 N 天重复) | | updateTime | string | 最近保存时间(回显只读) | ### #66b 超期提醒保存 — `POST /api/rule/customer/reminder/save` - 出处:`A7 后台管理\业务规则\客户规则\客户管理设置(超期未跟进提醒)\保存提醒规则.bru` + 同上控制器/Service - 请求参数:`masterEnabled / firstTriggerEnabled / secondIntervalEnabled`(number,否,**缺省=0 禁用**,不静默保留旧值)+ `firstTriggerDays / secondIntervalDays`(number,**必须非空且 >0**)。 - 出参(data):保存后全量配置(同 #66a)。 - 备注: - **绑定方式漂移(本次核对最重要的请求侧发现)**:`.bru` 请求示例为 JSON body 且 docs 自称「唯一 @RequestBody JSON 端点」,但控制器源码 `save(CustomerReminderRuleDTO dto)` **无 @RequestBody = 表单绑定**(ADR-0017),AS §2.13 亦记「`CustomerReminderRuleDTO` 表单」;demo 以 form-encoded 提交(L1420)实测通过。**Demo 断言按 form 提交,`.bru` 该件请求体示例为文档漂移**(全客户域唯一 JSON 例外是 crm-preference saved-view save,见 AS §0/§4)。 - 校验 **64023**:firstTriggerDays=0/负/null 或 secondIntervalDays=0/负/null → 「首次触发天数与二次提醒间隔天数必须为正整数」,不落库(task 面票「0 或 <0 报 64023」的精确化)。 - 保存即生效;Job 每轮只补「下一条未建到期提醒」(升级提醒链语义,remindSeq 连续递增)。 ### #69 公海池配置 —(后端无端点、原型无页) - **一行结论**(02 票收口):后端 crm-rule 客户域仅 `CustomerDedupRuleController` + `CustomerReminderRuleController` 两控制器(目录实测,无 CustomerPool*Controller);`.bru` 客户规则下仅上述 4 件(`线索规则\公海池*.bru` 7 件属**线索域**不得混入;客户公海=固定准入规则的虚拟池)——**无参数表**,Demo 按纪律做**只读占位卡**明示未实现(POOL-CFG,现象文案归 06 号票,引用本结论)。 --- ## 十、字典与枚举(联系人/流程/规则) | 字典/枚举 | 值域(code=文案) | 出处 | |---|---|---| | contact_source(联系人来源) | **group_meeting=组会 / group_activity=组局 / referral=转介绍 / other=其他** | 权威 seed:`crm-dict/.../config/DictDataInitializer.java` L362-367(GroupSeed("contact_source"),与 customer_contact.source 列注释口径一致) | | └ demo 硬编码漂移 | demo dlgContact L1151 硬编码 `contact_source_01=组会 / _02=组局 / _03=转介绍 / _04=其他`——**code 为字典改版前遗留,与权威 seed 不符**(列表侧 sourceName 已走 `/api/dict/item/enabled-list?groupCode=contact_source` 动态拉取✅,L623/629);`.bru` 顶级联系人列表/按客户查联系表示例中 `"source":"contact_source_01"` 同为陈旧真值 | demo index.html L1151;DictDataInitializer L364 | | job_title(职务) | 运行时**两级树**(一级类别/二级职务 code,含档位 level 1-10),demo `ensureDicts` 走 `/api/dict/item/tree?groupCode=job_title` 动态拉取✅(quickAdd/batchEdit 用 code 联动补全;create/edit 用 jobTitleName 自由文本+jobTitleId) | demo L627-630;ADR-0036;ContactQuickAddParam | | transfer_reason(交接原因) | resign=员工离职 / transfer_post=岗位调动 / region_adjust=区域调整 | 发起交接.bru;TransferInitiateDTO;AS §3 | | 交接单状态 | 0=待分配 → 1=已完成(全分配自动置位 D23);明细 assignStatus 0=待分配 1=已分配 | TransferDetailDTO;AS §3 | | 导入 importMode(客户/联系人导入同构) | APPEND_ONLY=仅新增 / UPDATE_ONLY=仅更新 / UPSERT=新增或更新 | 两导入 upload .bru;ImportPageParam | | 导入 duplicateStrategy | SKIP=不导入(缺省)/ OVERWRITE=覆盖导入 | AS §2.12 F8;两 upload .bru | | 导入任务状态 | 0=待确认(DRAFT) / 1=处理中(RUNNING) / 2=已完成(DONE) / 3=已失败(FAILED);两导入同构 | ContactImportResultDTO/ImportResultDTO | | 预校验判定 verdict | INSERT / UPDATE / FAIL / SUSPECT | ContactImportPreviewDTO.Row | | dedup nameMatchMode | 1=精确 / 2=模糊(出厂 2) | CustomerDedupRuleDTO;CustomerRuleConstants | | customerStage(交割预览行) | 1=潜在 / 2=重潜 / 3=已成交 | TransferPreviewDTO;AS §3 | | 客户/交割单编号 | 客户 `KH+yyyyMMdd+4位`;交割 `JG+yyyyMMdd+4位` | AS §3 | --- ## 十一、脱敏专节(客户域脱敏形态汇总) | # | 字段/位置 | 是否脱敏 | 形态示例 | 权威出处 | |---|---|---|---|---| | 1 | 联系人电话·列表(contact/page、contacts?customerId=) | **是**(服务端) | `138****0001`(前3+****+后4) | `ContactPhoneMasker` javadoc「所有人默认脱敏」;CustomerContactServiceImpl L131/L150 | | 2 | 联系人电话·详情/搜索(contact/detail、contact/search) | **是** | `138****0101`(`.bru` 实测) | ContactPhoneMasker;CustomerContactServiceImpl L113/L166;联系人详情.bru | | 3 | 图谱节点 phoneMasked | **是**(字段名即承诺) | `138****9901`(`.bru` 实测) | GraphNodeDTO @Schema;图谱全量读.bru | | 4 | 联系人导出 xlsx 电话列 | **是**(口径同列表,不因导出放开) | 列头即「手机号(脱敏)」 | ContactExportRow @ExcelProperty;CustomerContactEnhanceServiceImpl L307 | | 5 | check-phone 命中行 masked=true | **行级脱敏**(其余 5 字段全 null=只提示「存在匹配记录」) | `{"masked":true}` | ContactPhoneHitDTO javadoc;service L177-183 | | 6 | similarHits.masked(名称相似命中,#22 消费侧) | **行级脱敏**(无权命中 customerId/customerName/ownerName/customerStage/archiveStatus 全 null) | `{"masked":true}` | CustomerSimilarHitDTO @Schema;AS §0 脱敏现状 | | 7 | 客户 creditCode(统一社会信用代码) | **否(V1 明文出参)** | — | AS §0/§5.4:详情敏感信息三档 V1 未纳入;creditCode 仅作查重硬拦与导入匹配锚 | | 8 | 证件号 | **无此字段**(客户域出参无证件号列;三档脱敏定稿划出) | — | AS §5.4 | | 9 | 脱敏形态防御 | 短号(<8 位)全遮 `****`;空→null(Demo 显 `--`);**含 `*` 的串提交回 edit/batchEdit=不改号** | `****` | ContactPhoneMasker.mask/isMaskedForm | **明文唯一路径**:`GET /api/customer/contact/reveal?id=`(data=明文手机号;埋点落 `customer_contact_reveal_log`)。Demo 侧 reveal 态:`revealed[id]` 列表/图谱共享,「明文查看」=全量逐个 reveal(index.html L609/L1848-1864)。 **与「三档脱敏定稿」的关系**:AS §5.4 定稿**划出**客户域「敏感信息三档脱敏 + 查看日志、合并客户、客户导出」(V1 范围外)——即客户级手机号/证件分档脱敏与「显示完整金额」开关不落地(G 系豁免);而**联系人电话全员脱敏**是 contact-graph effort「所有人默认脱敏」map 拍板先行落地(票 04),不属三档定稿范畴,两者不冲突:前者=客户档案敏感分档(未做),后者=联系人单字段全量脱敏(已做)。Demo 断言按本表第 1-6 行执行,第 7-8 行如实渲染明文/无字段。 **`.bru` 明文示例漂移清单**(demo 冒烟不得按示例明文断言):顶级联系人列表.bru(13812340001)、联系人全局搜索.bru(13812340001)、按客户查联系人列表.bru(13812340002/0001)三件示例为票 04 脱敏上线前采集的陈旧真值;联系人详情.bru 与图谱全量读.bru 为脱敏后真值。 --- ## 十二、来源文件清单 **Bruno(`D:\code\crm-api-docs\`)** | 分组 | 文件 | |---|---| | 联系人独立菜单 | `A4 客户管理\联系人\顶级联系人列表.bru`、`联系人详情.bru`、`联系人全局搜索.bru`、`新增联系人.bru`、`编辑联系人.bru`、`删除联系人.bru`、`电话软提示.bru`、`读取列偏好.bru`、`保存列偏好.bru`、`读取视图形态.bru`、`保存视图形态.bru` | | 联系人页签增强/图谱/导入 | `A4 客户管理\客户详情\联系人\快速添加联系人.bru`、`电话明文查看.bru`、`联系人导出.bru`、`页内批量编辑.bru`;`客户详情\联系人图谱\图谱全量读.bru`、`图谱整体保存.bru`;`客户详情\联系人导入\下载导入模板.bru`、`上传导入文件并预校验.bru`、`确认执行导入.bru`、`任务结果.bru`、`失败明细与重复明细.bru`、`最近导入记录.bru` | | 联系人列表(页签 2 数据源) | `A3 商机管理\销售机会\按客户查联系人列表.bru` | | 客户交割 | `A4 客户管理\客户交割\发起前客户预览.bru`、`发起交接.bru`、`交接记录分页.bru`、`交接详情.bru`、`客户分配.bru`、`可分配销售列表.bru` | | 客户导入 | `A4 客户管理\我的客户\客户导入\下载导入模板.bru`、`上传导入文件并预校验.bru`、`确认执行导入.bru`、`任务结果.bru`、`失败疑似重复明细.bru`、`最近导入记录.bru` | | 规则族 | `A7 后台管理\业务规则\客户规则\客户管理设置(客户查重设置)\读取查重设置.bru`、`保存查重设置.bru`;`客户管理设置(超期未跟进提醒)\当前提醒规则.bru`、`保存提醒规则.bru` | **后端源码(`D:\code\crm-backend-matt\`)** - crm-customer `domain/dto/`:ContactDTO、GraphDTO、GraphNodeDTO、GraphEdgeDTO、ContactBatchResultDTO、ContactPhoneHitDTO、ContactImportPreviewDTO、ContactImportResultDTO、ContactImportFailDTO、TransferPreviewDTO、TransferInitiateDTO、TransferDetailDTO、AssignableUserDTO、ImportPreviewDTO、ImportResultDTO、ImportFailDTO、CustomerSimilarHitDTO、domain/excel/ContactExportRow、ContactImportTemplateRow、CustomerImportRow - crm-customer `domain/param/`:ContactPageParam、ContactBatchEditParam、ContactQuickAddParam、ContactImportPageParam、GraphSaveParam、TransferPageParam、ImportPageParam;crm-base `domain/param/BaseParam` - crm-customer `controller/`:CustomerContactImportController、CustomerImportController、CustomerTransferController;`service/impl/`:CustomerContactServiceImpl、CustomerContactEnhanceServiceImpl、CustomerImportServiceImpl(headers/守门常量);`util/ContactPhoneMasker` - crm-rule:`controller/CustomerDedupRuleController.java`、`CustomerReminderRuleController.java`、`domain/dto/CustomerDedupRuleDTO.java`、`CustomerReminderRuleDTO.java`、`service/impl/CustomerDedupRuleServiceImpl.java`、`CustomerReminderRuleServiceImpl.java`、`constant/CustomerRuleConstants.java` - crm-dict:`config/DictDataInitializer.java`(contact_source seed L362-367) **文档/台账(`D:\code\crm-backend-matt\.scratch\`)** - `customer-module/API-SUMMARY.md`(§0 通用/脱敏现状、§2.4、§2.7、§2.7b、§2.11、§2.12、§2.13、§3 值域、§4、§5.4/§5.6) - `customer-demo-ref/assets/coverage-matrix.md`(入口编号权威);`customer-demo-ref/map.md` - `customer-frontend-handover/gap-report.md`(豁免台账 D-G1/G5);`customer-contact-graph/e2e-report.md`(D-G1 根因) - `customer-e2e/demo/index.html`(fork 基线;引用行号基于此原文件);`customer-e2e/seed-ids.json`