联系人族 + 重流程 + 规则族 — 字段逐入口对齐表(票 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<T>,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(编辑页传自身) |
| 字段路径 |
类型 |
口径/文案 |
脱敏 |
| [].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