# 逐入口字段对齐验收清单(票 07 产出) > 用途:冒烟断言 + 人工验收双用。范围 = 覆盖矩阵(01)「缺口动作≠豁免」全部入口;豁免入口不展开(见 06 号票面板 C 区)。 > 权威出处缩写:`.bru` 相对 `D:\code\crm-api-docs\`;`[DTO:x]`/`[PARAM:x]` = `crm-customer/src/main/java/com/crm/customer/domain/{dto,param}/x.java`;`[PORT:x]` = 同树 `port/x.java`;crm-rule = `crm-rule/src/main/java/com/crm/rule/`。 > 两部分正文各自带「通用口径」与「字典枚举/脱敏专节」,先读下放**校准注记**再查表。 ## 跨票校准注记(实施/冒烟必须遵守,覆盖旧口径) | # | 校准 | 影响面 | |---|---|---| | K1 | **G4 收窄为「两件」**:detail 实测 `createTime/updateTime` 经 BaseDTO 有值,仅 `creatorId/updaterId` 不暴露 → 四件套只有「创建人/更新人」恒"--" | Demo G4 角标文案、冒烟步 13 断言 | | K2 | **关联项目页签有真实端点**(`CustomerProjectQueryPort` 已建,当前 Noop 实现 → 数据空):页签接线真端点、空态显"--",不做硬占位 | 矩阵 #42「无端点」过时;实施票 11 | | K3 | **看板 summary 字段 = `cnt`/`groupValue`**(demo 旧代码读 count/name/key 必显"--");cards 必传 groupValue,summary 显式传 groupColumn | 实施票 10 | | K4 | **oplog action 权威值域 18 种**(含 GRAPH_EDIT;AS §3「17 种」未同步) | 实施票 11 筛选下拉 | | K5 | **projectCount 三方漂移**(AS 写恒 null、SQL 恒 0、E2E 序列化 "0"):渲染按实返回,"--"兜底 | 列表列口径 | | K6 | **contact_source 真值 = group_meeting/group_activity/referral/other**(crm-dict DictDataInitializer);demo 硬编码 contact_source_01..04 为字典改版前遗留,须改字典驱动 | 实施票 12/13 | | K7 | **reminder save 控制器实为表单绑定**(.bru 自称 JSON 例外不实);demo form 提交正确,不改 | 冒烟断言口径 | | K8 | **focus-batch 返回 Void**(其余批量端点 BatchResult):UI 以 toast「已关注 N 条」自算 | 实施票 10 | | K9 | **quick-create `.bru` 漏 `confirmSimilar`、误标星级选填**(DTO 全 7 必填):以 DTO 为准 | 实施票 12 | | K10 | **联系人电话全读路径脱敏 `138****1234`**(短号全遮;导出同口径;明文唯一通道 reveal);`.bru` 示例明文系漂移;similarHits/check-phone 行级 masked;核心域其余字段 V1 明文(creditCode 明文回显) | 冒烟脱敏断言 | | K11 | **transfer/page `.bru` 示例 status=2 超值域**(权威 0/1);**reminder 64023 精确条件 = 两天数均须非空且 >0** | 冒烟断言 | --- # 第一部分 · 核心域(矩阵 #1-#49) > 用途:`.scratch/customer-demo-ref/demo/index.html` 的冒烟断言 + 人工验收双用。入口编号对齐 `assets/coverage-matrix.md`(缺口动作≠豁免)。 > 契约权威优先级:**Bruno `.bru` 出参示例 > DTO/Param 源码 > API-SUMMARY(下称 AS,`.scratch/customer-module/API-SUMMARY.md`)**。字段名逐字取自源码/`.bru`;「示例」列取 `.bru` E2E 实测真值或任务书 seed。 > **envelope**:统一 `Result = {code, success, message, data}`(code=0 成功,错误码见 AS §1);本表只写 `data` 内字段。 ## 通用口径(各入口共用,正文不再重复) - **分页请求**(`BaseParam`,crm-base):`current`(默认 1)/ `size`(默认 10,上限 500)/ `keyword` / `orderBy` / `asc`(默认 false=倒序)。 - **分页出参**(`PageResult`):`content[]` / `total` / `size` / `current` / `pages` / `empty`(=total==0)。 - **Long 序列化**:雪花 id 与 Long 计数 JSON 序列化为**字符串**(E2E 实测 `"opportunityCount":"0"`、`"total":"5"`);前端勿做 Number 运算,判空按 `=== null` / `!= null`。 - **时间格式**:LocalDateTime `yyyy-MM-dd HH:mm:ss`;LocalDate `yyyy-MM-dd`;表单提交 LocalDateTime 用 ISO `T` 分隔(空格分隔报 400,见 #40 follow/add)。 - **空值"--"口径**:demo 渲染 `value || '--'`(grid 兜底,旧 demo L1124-1135 同);契约 null 字段显 `--`,不显 0/空串。 - **脱敏总述**:核心域唯一脱敏位=查重两 DTO 的 `masked` 布尔标志——无权命中时 `customerId/customerName/ownerName/customerNo` 等**业务字段不透出(null)+ masked=true**,前端只渲染「存在匹配记录」(check-name)/「系统已存在该企业」(check-credit-code)。客户列表/详情 V1 **无字段级脱敏**:`unifiedCreditCode`、`presidentClassPhone` 等明文回显(E2E 实测 `"13800000001"`;敏感信息三档脱敏定稿划出,AS §0/§5.4)。联系人电话脱敏 + `contact/reveal` 明文端点属 contact 域,不在本表。 - **冒烟 seed**(e2e 种子库):admin `userId=739564171091247104`(罗伟健);客户 `750844477165273088`(e2c-全字段-科技,编号 KH202609030010,负责人=admin);公海客户 `750844487390986240`(e2c-公海-甲,无主);联系人 `750844500171030528`(张关键)。 --- ### #1 workspace 列表分页 — `POST /api/customer/workspace/page`(mine/overview/pool 三 workspace 共用一份契约) - 出处:`A4 客户管理\我的客户\workspace 列表分页.bru`(客户总览/客户公海同名 .bru 各一件)+ `crm-customer/.../domain/param/CustomerWorkspacePageParam.java` + `domain/dto/CustomerListRowDTO.java`(出参 SQL 见 `mapper/CustomerMapper.java` pageWorkspace) - 数据范围:overview=裸表+`@DataScope`(管理员可见全部);mine=负责人**或**协同人(子查询包裹);pool=owner 空且按部门可见范围(子查询包裹)。 **请求参数**(form 表单绑定;含 BaseParam 五参): | 名 | 类型 | 必填 | 示例 | 口径 | |---|---|---|---|---| | workspace | string | 是 | `mine` | `mine`/`overview`/`pool`;未知值 67001「未知 workspace」 | | viewType | string | 否 | `ASSIGNED` | 内置五视图(硬编码不落库),空=基础集;适配矩阵见下 | | savedViewId | string | 否 | | 叠加自定义视图(#7)检索条件于数据集之上,不改 @DataScope 边界;**RECENT 视图不接**;视图不存在静默不叠加;**board/cards 不接** | | archiveStatus | number | 否 | `1` | 1 有效 / 2 已归档;默认 1(D19 归档=删除语义) | | customerStage | number | 否 | `1` | 1 潜在 / 2 重潜 / 3 已成交 | | customerType | string | 否 | `customer_type_01` | customer_type 字典 code | | customerStarLevel | number | 否 | `3` | 1~5 | | relationStarLevel | number | 否 | `3` | 1~5 | | industryCode | string | 否 | `gov` | 行业一级 code | | provinceCode | string | 否 | `440000` | 省国标 code | | groupColumn | string | 否 | `stage` | 仅看板两端点有意义:白名单 `stage`/`star`/`relation`,越界静默忽略 | | groupValue | number | 否 | `1` | 仅 board/cards(与 groupColumn 配对) | | current / size / keyword / orderBy / asc | — | 否 | `1` / `50` / `恒信达` | BaseParam;keyword 按名称/编号等业务列模糊 | **viewType 内置五视图 × workspace 适配矩阵**(越界视图不硬拒,SQL 自然空集;F7 口径:ownerOnly 等布尔未做,单 viewType 参数承载五视图): | viewType | 文案 | mine | overview | pool | |---|---|---|---|---| | ASSIGNED | 我负责的 | ✅ | ✅ | ❌ | | COLLABORATING | 我协同的 | ✅ | ✅ | ❌ | | FOLLOW_UP_DUE | 待跟进(next_follow_time 到期未跟) | ✅ | ❌ | ❌ | | FOCUSED | 我关注的 | ✅ | ✅ | ✅ | | RECENT | 最近访问(customer_view_log 倒序锚点) | ✅ | ✅ | ✅(不接 saved-view) | **出参**(`PageResult`,data 内 24 字段;示例值=E2E 公海行 + mine 行): | 字段路径 | 类型 | 口径/文案 | 脱敏 | 示例 | |---|---|---|---|---| | content[].id | Long→str | 客户 ID | - | `"750844487390986240"` | | content[].customerNo | string | 客户编号 KH+yyyyMMdd+4 位 | - | `"KH202609030020"` | | content[].customerName | string | 客户名称(固定列不可隐藏) | - | `"e2c-公海-甲"` | | content[].customerType | string | 字典 code(前端字典缓存翻译,**后端不回显名**) | - | `"customer_type_01"` | | content[].provinceCode / cityCode / districtCode | string | 国标 code(名称前端字典渲染) | - | `"440000"` / `"440100"` / `"440103"` | | content[].industryCode / industryChildCode | string | 行业一级/二级 code | - | `"gov"` / `"gov_1"` | | content[].customerStage | number | 1 潜在 2 重潜 3 已成交 | - | `1` | | content[].customerStarLevel / relationStarLevel | number | 1~5 | - | `3` / `3` | | content[].strategicAgreementLevel | number | 战略协议等级快照,null=未签 | - | `null` | | content[].vipCustomerLevel | number | 已成交 VIP 客户等级快照,null=无 | - | `null` | | content[].opportunityCount | Long→str | **真算**:SQL 标量子查询 `opportunity_customer join opportunity`(oc.delete_key=0 且 o.deleted=0) | - | `"0"` | | content[].projectCount | Long→str | **恒空**:SQL `SELECT 0 AS project_count`(A5 未接);AS §2.1 写「恒 null 显--」、DTO @Schema 注「恒 0」、E2E 实测 `"0"`——demo 按「恒空显 --」渲染即可,值不具业务含义 | - | `"0"` | | content[].lastValidFollowTime | string | 最近有效跟进时间(超期锚点) | - | `"2026-09-03 22:55:18"` | | content[].ownerUserId | Long→str | 销售负责人 ID;**pool 行恒 null** | - | `null`(pool)/ `"739564171091247104"`(mine) | | content[].ownerUserNameSnapshot | string | 负责人姓名快照 | - | `null` / `"罗伟健"` | | content[].ownerDeptId | Long→str | 销售部门 ID 快照 | - | `"744841292348915712"` | | content[].ownerDeptNameSnapshot | string | 部门名快照 | - | `"广东保伦电子股份有限公司"` | | content[].enterPoolTime | string | 进入公海时间;**pool 固定列可排序**(D29 pool 默认排序=enter_pool_time 倒序;其余 workspace 默认 create_time 倒序) | - | `"2026-09-11 11:45:49"` / `null` | | content[].archiveStatus | number | 1 有效 2 已归档 | - | `1` | | content[].lastViewTime | string | 最近访问时间;**仅 RECENT 视图非 null**(同时是 RECENT 排序锚点) | - | `null` | | content[].focusFlag | number | 是否关注:customer_focus 行存在=1(**按当前登录用户**),0=未关注;三 workspace 通用 | - | `0` | | total / size / current / pages | Long→str | 分页元信息 | - | `"5"` / `"1"` / `"1"` / `"5"` | | empty | boolean | total==0 | - | `false` | - 备注:原型差异列=mine/overview 加负责人+部门四件,pool 加 enterPoolTime 且无负责人列;demo 现 11 列,类型/行业/战略协议等级/VIP 等级/进入公海时间列未渲染(矩阵 #11 字段级挂 07 票)。字典列后端一律回 code 不回名(CustomerListRowDTO 类注释明示)。 --- ### #9 看板分组汇总 — `POST /api/customer/workspace/board/summary` - 出处:`A4 客户管理\我的客户\看板列表\看板分组汇总.bru`(客户总览同名一件;**公海无看板 .bru**)+ `domain/dto/CustomerBoardSummaryDTO.java` + `service/impl/CustomerWorkspaceServiceImpl.java` - 请求参数:复用 `CustomerWorkspacePageParam`(同 #1),**groupColumn 必填**(stage/star/relation),**无需 groupValue**;分页/keyword 意义有限(同 WHERE 口径)。示例载荷:`workspace=mine&groupColumn=stage¤t=1&size=1&keyword=恒信达` - 出参(`List`,数组元素两字段): | 字段路径 | 类型 | 口径/文案 | 示例 | |---|---|---|---| | [].groupValue | number | 分组取值(stage 1~3 / star 1~5) | `1` | | [].cnt | Long→str | 该分组客户数(**仅返回有数据分组**,空列骨架前端补全;本期重潜/已成交恒空=D20 预期) | `"28"` | - 备注(**demo 旧代码字段名漂移**):旧 demo `loadSummary`(L590-592)读 `it.count` / `it.name`——**契约字段是 `cnt` / `groupValue`,旧读法必显 "--"**,改造时按 `groupValue`→文案、`cnt`→数字渲染。E2E 实测:`[{"groupValue":1,"cnt":"28"},{"groupValue":2,"cnt":"6"},{"groupValue":3,"cnt":"3"}]`。 ### #9 看板单列卡片 — `POST /api/customer/workspace/board/cards` - 出处:`A4 客户管理\我的客户\看板列表\看板单列卡片.bru`(总览同名)+ 同上源码 - 请求参数:同 #1 + `groupColumn`(stage/star/relation)+ **`groupValue` 必填**(缺省 67001「看板取卡必须指定分组值」)。示例载荷:`groupColumn=stage&groupValue=2&workspace=mine¤t=1&size=20&keyword=恒信达` - 出参:`PageResult`——**与列表 #1 逐字段同构**(24 字段,E2E 卡片样例:customerStage=2、customerStarLevel=4、ownerUserId=`"739564171091247104"`、total=`"6"`)。 - 备注:仅 mine/overview 可用;**pool 调看板两端口一律 67001「公海无卡片视图,看板仅支持客户总览/我的客户」**(源码 `assertBoardAllowed` 守卫在,AS §2.1 文档口径一致;GR §六.1 记 jar 实测 code=0 漂移待 04 票核实);board/cards 不接 saved-view;summary 与 page/cards 同一 WHERE 口径,数字自洽。 --- ### #7 preference 自定义视图四端点(saved-view) — `GET|POST /api/preference/view/list|save|delete|set-default` - 出处:**A4 无 saved-view `.bru` 文档**(契约源=AS §2.14 + crm-preference 源码 `controller/SavedViewController.java`、`domain/dto/SavedView.java`、`SavedViewCondition.java`);示例形态借 `A3 商机管理\销售机会\自定义视图\*.bru` 四件(字段结构同源,商机域 scopeKey 实测 `opportunity`) - **scopeKey**(客户域,`CustomerConstants` L67-69):`customer.mine` / `customer.overview` / `customer.pool`;userId 取登录态,不接受前端传入。 - 端点逐个: | 端点 | 请求参数 | 出参 data | |---|---|---| | `GET /api/preference/view/list?scopeKey=` | scopeKey | `List`,按 seqNo 排序;出参不回显 scopeKey | | `POST /api/preference/view/save?scopeKey=` | scopeKey(query 表单参)+ **`@RequestBody SavedView` JSON**——全平台唯一 JSON 例外(ADR-0017 范围外) | `String` viewId(新建/覆盖后服务端生成) | | `POST /api/preference/view/delete?scopeKey=&viewId=` | scopeKey、viewId | `null` | | `POST /api/preference/view/set-default?scopeKey=&viewId=` | scopeKey、viewId(单值互斥,设默认自动取消原默认) | `null` | - **SavedView 字段**(JSON body): | 字段 | 类型 | 必填 | 口径 | |---|---|---|---| | viewId | string | 新建可空;编辑/删除/设默认须回传 | 服务端生成 32 位 hex(样例 `"e7b9595f7ea54641b093e9d6bfb096f9"`) | | name | string | 是 | 检索名称(用户填) | | conditions | array | 否 | 本期前端只提交 1 组(契约按列表预留复合扩展) | | conditions[].field | string | - | 业务方字段池稳定 code(如 `industryCode`) | | conditions[].operator | string | - | 业务方自定义:等于/不等于/包含/不包含/为空/不为空(demo 用 eq/like 等) | | conditions[].value | string | - | 为空/不为空操作符时可为 null | | sortField / sortDirection | string | 否 | 排序字段 key / `asc`|`desc` | | isDefault | boolean | 否 | 默认视图图钉,单值互斥 | | seqNo | number | 否 | 列表排序序号 | - 备注:内置五视图(viewType)不走本接口;workspace/page 的 `savedViewId` 消费本接口存的 viewId(#1);视图为用户私有。 ### #8 preference 视图形态两件 — `GET|POST /api/preference/view-form/get|save` - 出处:`A4 客户管理\我的客户\读取视图形态.bru` + `保存视图形态.bru`(总览/公海同名共 6 件)+ `ViewFormController.java`、`ViewFormServiceImpl.java` - `GET /api/preference/view-form/get?scopeKey=` → `String`:`list`/`split`/`board`;**null=未设置,前端默认 list**(E2E:`"data": "list"`) - `POST /api/preference/view-form/save`(form:`scopeKey` + `viewForm`)→ `null`;白名单 {list,split,board},非法 68001(平台段位,不在 67xxx) - 备注:scopeKey 同 #7 三码位(`customer.mine/overview/pool`)。文档口径「pool 仅 list/split,board 被拒 680xx」**实现侧无 pool 特判——pool 保存 board 实回 code=0**(AS §2.14 ⚠ I-06 r2 漂移),前端不得依赖拒绝行为。 --- ### #13-19 归属/关注/重点 动作族(合并表,每端点一行;全 POST,id/ids 走查询参数) - 出处:`crm-customer/.../controller/CustomerOwnershipController.java`、`CustomerFocusController.java`;.bru 逐件:`客户公海\领取客户.bru`、`批量领取.bru`、`分配客户.bru`、`批量分配.bru`、`抛公海.bru`、`批量抛公海.bru`、`归档客户.bru`、`批量归档.bru`、`恢复客户.bru`、`关注客户.bru`、`取消关注客户.bru`、`批量关注客户.bru`、`客户详情\标记重点客户.bru`、`取消重点标记.bru` - 示例值:单条 `id=750844487390986240`;批量 `ids=750844487390986240`(重复键 `ids=..&ids=..`);分配目标 `userId=744842318024015872`。 | 端点 | 参数 | 返回 data | 主要错误码 | 口径 | |---|---|---|---|---| | `POST /api/customer/focus?id=` | id | `null`(Result) | 67002 | 幂等;行存在性=关注态;不写 oplog | | `POST /api/customer/unfocus?id=` | id | `null` | — | 幂等;物理删行,连带清重点标记 | | `POST /api/customer/focus-batch?ids=` | ids[] | **`null`(注意:非 BatchResult**——controller 循环逐个 focus 后恒 success) | 67002 | 逐个幂等关注,对称商机 | | `POST /api/customer/star?id=` | id | `null` | 67002 | 幂等;未关注自动建行 starred=1;不写 oplog | | `POST /api/customer/unstar?id=` | id | `null` | — | 幂等;保留行仅置 starred=0 | | `POST /api/customer/assign?id=&userId=` | id、userId | `null` | 67002/67012 | 目标人员启用在职+非原负责人;不校验进行中商机 | | `POST /api/customer/assign-batch?ids=&userId=` | ids[]、userId | `BatchResult` | 67002/67012 | 目标人员统一校验一次(无效**整批拒绝**);其余逐条独立事务 | | `POST /api/customer/claim?id=` | id | `null` | 67002/67004 | 校验仍在公海;D25 领取即赋 last_valid_follow_time 锚点(为空时赋当下) | | `POST /api/customer/claim-batch?ids=` | ids[] | `BatchResult` | 67002/67004 | 逐条独立事务,部分成功 | | `POST /api/customer/release-pool?id=` | id | `null` | 67002/67007 | 清 owner+协同人(部门锚点保留),软删 team_member;进行中商机 67007 阻断 | | `POST /api/customer/release-pool-batch?ids=` | ids[] | `BatchResult` | 67002/67007 | 逐条独立事务,部分成功 | | `POST /api/customer/archive?id=` | id | `null` | 67002/67004/67006 | 仅销售负责人;归档=删除语义(D19);进行中商机 67006 阻断 | | `POST /api/customer/archive-batch?ids=` | ids[] | `BatchResult` | 67002/67004/67006 | **整批预检:任一失败整批不执行**(预检失败=顶层非 0,E2E 实测顶层 `code:67006`);预检过后逐条独立事务 | | `POST /api/customer/restore?id=` | id | `null` | 67002/67004 | 仅销售负责人(已归档→有效) | - **BatchResult 结构**(`crm-base/.../result/BatchResult.java`):`{total:int, successCount:int, failCount:int, failures:[{id:Long→str, reason:string, message:string}]}`。`reason` 语义枚举(`CustomerBatchFailReason`):`NOT_FOUND / STATUS_NOT_ALLOWED / CONCURRENT_MODIFIED / ARCHIVE_BLOCKED / POOL_BLOCKED / ASSIGN_INVALID / UNKNOWN`。E2E 失败行样例:`{"id":"753612204858671104","reason":"STATUS_NOT_ALLOWED","message":"客户已被领取,不在公海"}`。 - **错误码含义**(AS §1):67004 当前状态不允许(归档/恢复/抛公海守卫);67006 归档被阻断:存在进行中商机;67007 抛公海被阻断:存在进行中商机;67012 分配非法(明细已分配/对象越权/为发起人本人/为当前负责人)。 - 单条动作 CAS 乐观锁 + 写 oplog(TRANSFER/CLAIM/POOL/ARCHIVE/RESTORE 均入 #45 oplog 值域);关注/重点族不写 oplog。 --- ### #21 新建客户 — `POST /api/customer/create`(form 绑定 `CustomerCreateDTO`,禁 @RequestBody) - 出处:`A4 客户管理\我的客户\新增客户.bru` + `domain/dto/CustomerCreateDTO.java`;出参 `CustomerSaveResultDTO.java` + `CustomerSimilarHitDTO.java` - 三层查重内建:L1 停输防抖(check-name,只提示)→ L2 名称相似 needConfirm 弹窗(未落库)→ L3 信用代码硬拦(67003)。`ownerUserId` 空=直接进公海并记 `enter_pool_time`(D25,锚点 NULL);有值=直接指派(快照服务端补齐,不信任前端传部门)。 - 请求参数(DTO 全字段,按区块分组;「必填」以 .bru 参数表+DTO @Schema 为准): | 名 | 类型 | 必填 | 示例 | 值域/口径 | |---|---|---|---|---| | customerName | string | 是 | `e2c-查重-撞码-003045` | 停输防抖触发相似查重 | | customerType | string | 是 | `customer_type_01` | customer_type 字典(6 值,见字典节) | | provinceCode / cityCode | string | 是 | `440000` / `440100` | 国标 code | | districtCode | string | 否 | `440103` | 国标 code | | industryCode | string | 是 | `gov` | 行业一级 code | | industryChildCode | string | 否 | `gov_1` | 行业二级 code | | customerStarLevel / relationStarLevel | number | 是 | `3` | 1~5 | | networkUnits | string | 否 | | 人脉关系单位 JSON 数组(标签名) | | ownerUserId | string(Long) | 否 | `739564171091247104` | 空=进公海;无效/停用 67012 | | companyDecisionMaker | string | 否 | `王总` | 公司决策人 | | isBizNegotiated | number | 是 | `0` | 1 是 / 0 否 | | directorVisitTime | string | 否 | | 总监近期拜访时间 yyyy-MM-dd HH:mm:ss | | isChild | number | 是 | `0` | 1 是 / 0 否(默认 0) | | parentCustomerId | string(Long) | 否 | | isChild=1 时必填,服务端防环校验 | | cooperationSystem | string | 否 | `OA对接` | 合作系统 | | nonCoopReason | string | 否 | | 不合作原因 | | joinedPresidentClass | number | 否 | `1` | 1/0/null(未选) | | presidentClassPerson / presidentClassPhone | string | 条件 | `王总` / `13800000001` | joinedPresidentClass=1 时 Person 必填 | | joinedProductClass | number | 否 | | 1/0/null | | productClassPerson / productClassPhone | string | 条件 | | joinedProductClass=1 时 Person 必填 | | unifiedCreditCode | string | 否 | `91440101E2CHXDA001` | 18 位数字或大写字母;撞码 L3 硬拦 67003 | | legalRepresentative | string | 否 | `王测试` | | | establishedDate | string | 否 | `2018-05-20` | yyyy-MM-dd,不得晚于当前日期 | | registeredCapital / businessScope / staffSize / annualRevenue / businessAddress | string | 否 | `1000万元人民币` 等 | 工商信息区文本 | | bizNameFromLookup | string | 否 | | 企查查确认的工商名称(只存不覆盖客户名称) | | remark | string | 否 | `e2e 实测备注` | ≤500 字 | | confirmSimilar | boolean | 否 | `true` | L2 弹窗后「仍要创建」重发置 true | - 出参(`CustomerSaveResultDTO`,data 内 5 字段):`id`(Long→str,落库成功时)、`needConfirm`(Boolean,true=名称相似需确认**未落库**)、`similarHits[]`(见 #22)、`customerNo`(落库成功时,`"KH202609110010"`)、`customerName`。 - 错误码:67001 入参非法 / 67003 信用代码撞码 / 67012 销售负责人无效 / 67015 查重设置单例缺失。 - E2E 落库成功样例:`{"id":"753584451841163264","needConfirm":null,"similarHits":null,"customerNo":"KH202609110010","customerName":"e2c-cg-A-098932"}`。 ### #22 名称相似查重 — `GET /api/customer/check-name?name=&excludeId=` - 出处:`A4 客户管理\客户查重\名称相似提示.bru` + `CustomerSimilarHitDTO.java`(L1 停输防抖 / L2 弹窗共用载荷;create/quick-create 内建查重同款) - 请求参数:`name`(string,必填,待查客户名称);`excludeId`(string,可选,编辑页传自身排除)。 - 出参(`List`,data 内 6 字段/行): | 字段路径 | 类型 | 口径 | 脱敏 | 示例 | |---|---|---|---|---| | [].customerId | Long→str | 命中客户 ID | **无权命中=null** | `"750844478243209216"` | | [].customerName | string | 命中客户名称 | 无权命中=null | `"e2c-恒信达科技有限公司"` | | [].ownerName | string | 命中客户负责人 | 无权命中=null | `"罗伟健"` | | [].customerStage | number | 1 潜在 2 重潜 3 已成交 | 无权命中=null | `1` | | [].archiveStatus | number | 1 有效 2 已归档 | 无权命中=null | `1` | | [].masked | boolean | 无权命中=true | **true 时以上五字段全 null,前端只渲染「存在匹配记录」** | `false` | - 备注:**masked 脱敏形态(契约=DTO 注释,E2E 无 masked=true 样例)**:不是打星号字符串,而是「字段置 null + masked=true + 固定文案」。匹配方式/阈值走查重设置(AS §2.13:nameMatchMode 1 精确 / 2 模糊,similarityThreshold 0~100 出厂 80;.bru docs 提 ngram 阈值 70/80/90 为旧口径,以查重设置单例为准)。错误码 67015(单例未落地时走出厂默认不报)。 ### #23 信用代码查重 — `GET /api/customer/check-credit-code?creditCode=` - 出处:`A4 客户管理\客户查重\信用代码查重.bru` + `CreditCodeCheckDTO.java` - 请求参数:`creditCode`(string,必填,18 位)。示例 `91440101MA9ABC1234`。 - 出参(`CreditCodeCheckDTO`,data 内 6 字段): | 字段路径 | 类型 | 口径 | 脱敏 | 示例 | |---|---|---|---|---| | exists | boolean | 是否已存在同码客户 | - | `true` | | masked | boolean | 无权命中=true | **true 时只提示「系统已存在该企业」** | `false` | | customerId | Long→str | 命中客户 ID | 有权可见时给 | `"750844478243209216"` | | customerName | string | 命中客户名称 | 有权可见时给 | `"e2c-恒信达科技有限公司"` | | customerNo | string | 命中客户编号 | 有权可见时给 | `"KH202609030011"` | | ownerName | string | 命中客户负责人 | 有权可见时给 | `"罗伟健"` | - 备注:L1 即时校验(失焦/企查查回填后调用);L3 保存时硬拦 67003;系统强制无开关(AS §2.13)。 ### #24 工商联想 — `GET /api/customer/company-lookup?companyName=`(**参数名是 companyName 不是 name**) - 出处:`A4 客户管理\我的客户\工商信息查询.bru` + `port/CompanyLookupPort.java` - 请求参数:`companyName`(string,必填,公司名称关键字)。示例 `恒信达`。 - 出参(`List`,9 字段/候选):`bizName` / `unifiedCreditCode` / `legalRepresentative` / `establishedDate`(LocalDate)/ `registeredCapital` / `businessScope` / `staffSize` / `annualRevenue` / `address`。 - 备注:**一期 stub 恒返回空数组**(E2E:`"data": []`),前端按「未查询到匹配企业」渲染,可手工录入;真实企查查接入后自动激活。前端职责:只回填当前为空的字段,已填字段再选须弹「受影响字段确认」;回填后立即调 check-credit-code;无结果/失败/超时均不阻断手工提交。 ### #25 编辑客户 — `POST /api/customer/edit?id=`(form 绑定 `CustomerUpdateDTO` + CAS) - 出处:`A4 客户管理\客户详情\编辑客户.bru` + `CustomerUpdateDTO.java` - 请求参数:查询参数 `id` + 表单(与 #21 create 的差异): - **多**:`version`(number,**必传**,乐观锁 CAS,不匹配报 **67005**「乐观锁冲突」;编辑回显取 `GET /detail` 的 `version` 带回)。 - **少**:无 `ownerUserId`(**归属字段不可改**——负责人/部门「✅(新增时)」;服务端忽略任何归属入参,归属变更走 assign/transfer)。 - 其余 33 字段与 create 同名同义(customerName…remark + confirmSimilar),必填区相同(isBizNegotiated/isChild 必填)。 - 出参:同 `CustomerSaveResultDTO`(needConfirm 相似确认流同样适用,`confirmSimilar=true` 重发)。 - 错误码:67001 / 67002 / 67003 / 67005。 - 备注(AS §2.3 edit 行注):冒烟实测需一并提交 `version`+`isBizNegotiated`+`isChild`,缺省因 DB NOT NULL 无默认而失败(关联 AS §5 D-05 已修,冒烟口径保守保留)。E2E 反例样例:`{"code":67005,"success":false,"message":"缺少乐观锁版本号","data":null}`。 ### #47 快速创建客户 — `POST /api/customer/quick-create`(`CustomerQuickCreateDTO` 最小集) - 出处:`A4 客户管理\我的客户\快速创建客户.bru` + `CustomerQuickCreateDTO.java` - 请求参数(7+1 字段,DTO `requiredMode=REQUIRED` 全 7 必填): | 名 | 类型 | 必填 | 示例 | |---|---|---|---| | customerName | string | 是 | `e2c-快创-448049` | | customerType | string | 是 | `customer_type_01` | | provinceCode / cityCode | string | 是 | `440000` / `440100` | | industryCode | string | 是 | `gov` | | customerStarLevel / relationStarLevel | number | 是 | `2` | | confirmSimilar | boolean | 否(默认 false) | 命中相似确认后重发置 true(返工票 04 补齐) | - 出参:`CustomerSaveResultDTO`(同 #21)。E2E needConfirm 样例:`{"needConfirm":true,"similarHits":[{"customerId":"753620871771324416","customerName":"e2c-t05qc-相似源甲科技有限公司","ownerName":"罗伟健","customerStage":1,"archiveStatus":1,"masked":false}],"customerNo":null,"customerName":null}`。 - 错误码:67001 / 67003。 - 备注(**两处 .bru 漂移,以源码/AS 为准**):① .bru 参数表把 industryCode/两星级标「否」且**漏 `confirmSimilar`**——DTO 源码有该字段(返工票 04 已补);② .bru docs 文本「QuickCreateDTO 无 confirmSimilar 字段」为陈旧描述。服务端默认:owner=当前用户、is_child=否、isBizNegotiated=否;查重三层与编号生成复用主表 create。 ### #48 商机侧客户搜索 — `POST /api/customer/search`(form 绑定 `CustomerSearchParam`) - 出处:**无 A4 `.bru`**(契约源=源码 `CustomerSearchParam.java` + `CustomerSearchItemDTO.java` + AS §2.4);A3 侧演示形态待 03 票 - 请求参数:BaseParam 五参(keyword=**名称/联系人/电话三维模糊**)+ `customerType`(string,字典 code 精确,可空)+ `archiveStatus`(number,1/2,可空=全部)+ `excludeCustomerIds`(Long 数组,重复键;商机候选池排除已关联客户,可空)。 - 出参(`PageResult`,行 9 字段): | 字段路径 | 类型 | 口径 | 示例 | |---|---|---|---| | content[].id | Long→str | 客户 id | — | | content[].customerNo | string | 客户编号 | — | | content[].customerName | string | 客户名称 | — | | content[].customerType | string | 字典 code | — | | content[].ownerUserId | Long→str | 销售负责人 id | — | | content[].ownerName | string | 负责人姓名(快照) | — | | content[].ownerDeptId | Long→str | 负责人部门 id | — | | content[].customerStage | number | 1 潜在/2 重潜/3 已成交 | — | | content[].archiveStatus | number | 1 有效/2 已归档 | — | - 备注:轻量出参;选中后走 `GET /api/customer/detail?id=` 带出全量;DataScope 注入可见范围。 --- ### #29 详情公共头部 — `GET /api/customer/detail-head?id=` - 出处:`A4 客户管理\客户详情\详情公共头部.bru` + `domain/dto/CustomerDetailHeadDTO.java` - 请求参数:`id`(string,必填,查询参数)。示例 `750844477165273088`。 - **副作用**:upsert `customer_view_log` 刷新「最近访问」锚点(F7 viewTouch 收口;UNIQUE(user_id,customer_id) 去重;高频行为不落 oplog)——冒烟断言 RECENT 视图排序前先打一次详情头。 - 出参(`CustomerDetailHeadDTO`,data 内 24 字段;示例=E2E 客户甲): | 字段路径 | 类型 | 口径/文案 | 示例 | |---|---|---|---| | id | Long→str | 客户 ID | `"750844477165273088"` | | customerNo | string | 客户编号 | `"KH202609030010"` | | customerName | string | 客户名称 | `"e2c-全字段-科技"` | | customerStage | number | 1 潜在 2 重潜 3 已成交(只读条,不可手动改) | `1` | | customerStageName | string | 阶段名回显(CustomerStageEnum) | `"潜在客户"` | | customerStarLevel / relationStarLevel | number | 1~5 | `3` / `3` | | strategicAgreementLevel | number | 快照,null=未签 | `null` | | vipCustomerLevel | number | 已成交 VIP 等级快照,null=无 | `null` | | archiveStatus | number | 1 有效 2 已归档(已归档态 badge 数据源) | `1` | | ownerUserId | Long→str | 销售负责人 ID(公海 null) | `"739564171091247104"` | | ownerUserNameSnapshot | string | 负责人姓名快照 | `"罗伟健"` | | ownerDeptNameSnapshot | string | 部门名快照 | `"广东保伦电子股份有限公司"` | | lastFollowSummary | string | 最近跟进摘要(最新一条跟进内容) | `"e2c-F08 跟进实测:电话沟通年度合作意向"` | | lastFollowTime | string | 最近跟进时间 | `"2026-09-11 11:45:33"` | | nextFollowTime | string | 待跟进提醒(未来最近一条下次跟进时间,null=无待跟进) | `"2026-09-12 22:34:06"` | | opportunityCount | Long→str | 关联商机数(外部反查,失败前端显 "--") | `"0"` | | projectCount | Long→str | DTO @Schema=「恒 null 显 --」;E2E 实测 `"0"`——按恒空渲染 | `"0"` | | wonProjectAmount | BigDecimal | 汇总卡①已中标项目总额——**恒 null 显 "--"**(A5/投标 seam 未接入) | `null` | | planEstimateAmount | BigDecimal | 汇总卡②跟进中方案预估总额——恒 null 显 "--"(方案卡 seam) | `null` | | ongoingOpportunityAmount | BigDecimal | 汇总卡③进行中商机总额——恒 null 显 "--"(可经 port 扩展后切真值) | `null` | | contractAmount | BigDecimal | 汇总卡④合同总额——恒 null 显 "--"(合同模块未建) | `null` | | paidAmount | BigDecimal | 汇总卡④累计回款——恒 null 显 "--"(与合同总额同卡) | `null` | - 备注:**金额 5 字段恒 null 显 "--" 是 grill Q18 契约:前端渲染 4 张汇总卡、显 "--" 不显 0**(矩阵 #29=P3-6 改造点:demo 未渲染卡)。错误码 67002。 ### #30 客户详情(页签1 客户信息 / 编辑页回显同源) — `GET /api/customer/detail?id=` - 出处:`A4 客户管理\客户详情\客户详情.bru` + `domain/dto/CustomerDetailDTO.java`(extends BaseDTO) - 请求参数:`id`(string,必填,查询参数)。错误码 67002。 - 出参(`CustomerDetailDTO`,data 内 48 字段=BaseDTO 3 + 本类 45;示例=E2E 客户甲): | 字段路径 | 类型 | 口径/文案 | 脱敏 | |---|---|---|---| | id | Long→str | 主键 | - | | createTime / updateTime | string | **经 BaseDTO 带出,E2E 实测有值** `"2026-09-03 22:27:53"`(见下方 G4 备注) | - | | customerNo | string | KH+yyyyMMdd+4 位序,只读 | - | | customerStage | number | 1 潜在 2 重潜 3 已成交(只读条) | - | | archiveStatus | number | 1 有效 2 已归档 | - | | version | number | 乐观锁版本号(**编辑回传**,E2E `3`) | - | | customerName | string | 客户名称 | - | | customerType / customerTypeName | string | code + **字典名回显**(`customer_type_01`/`总包`) | - | | provinceCode / cityCode / districtCode | string | 国标 code(名称前端字典渲染,后端不回名) | - | | industryCode / industryName | string | `gov`/`政府机关`(一级字典名回显) | - | | industryChildCode / industryChildName | string | `gov_1`/`人大政协`(二级字典名回显) | - | | customerStarLevel / relationStarLevel | number | 1~5 | - | | networkUnits | string | 人脉关系单位 JSON 数组 | - | | ownerUserId | Long→str | 销售负责人 ID(公海 null) | - | | ownerUserNameSnapshot / ownerDeptId / ownerDeptNameSnapshot | — | 负责人姓名/部门 ID/部门名快照 | - | | enterPoolTime | string | 进入公海时间(公海客户才有值) | - | | lastValidFollowTime | string | 最近有效跟进时间(超期锚点) | - | | strategicAgreementLevel / vipCustomerLevel | number | 快照(null=未签/无) | - | | companyDecisionMaker | string | 公司决策人 | - | | isBizNegotiated | number | 是否商机勾兑 1/0 | - | | directorVisitTime | string | 总监近期拜访时间 | - | | isChild / parentCustomerId / parentCustomerNameSnapshot | — | 子客户标志/父客户 ID/父客户名快照 | - | | cooperationSystem / nonCoopReason | string | 合作系统/不合作原因 | - | | joinedPresidentClass / presidentClassPerson / presidentClassPhone | — | 总裁班三件(**电话明文**,E2E `"13800000001"`) | **无脱敏** | | joinedProductClass / productClassPerson / productClassPhone | — | 产品班三件(电话明文) | **无脱敏** | | unifiedCreditCode | string | 统一社会信用代码(E2E `91440101E2CTEST001` 明文) | **无脱敏(V1 三档脱敏划出)** | | legalRepresentative / establishedDate / registeredCapital / businessScope / staffSize / annualRevenue / businessAddress / bizNameFromLookup | — | 工商信息区(establishedDate=LocalDate) | - | | remark | string | 备注 | - | - 备注(**G4 校准**):矩阵 #30 记档「系统信息四件套 creatorId/createTime/updaterId/updateTime 不在 DTO 恒"–"」——**逐字核对源码后应修正为:`creatorId`/`updaterId` 确实不在 CustomerDetailDTO(无字段);但 `createTime`/`updateTime` 由 BaseDTO 继承带出且 E2E 实测有值**。demo 现渲染四件套恒 "--" 是保守可行口径;若要显示创建/更新时间可直接取值,只有操作人两件必须 "--"。 ### #40 跟进记录分页 — `GET /api/customer/follow/page?id=` + 新增跟进 — `POST /api/customer/follow/add?id=` - 出处:`A4 客户管理\客户详情\跟进记录\跟进记录分页.bru` + `新增跟进.bru` + `domain/param/FollowPageParam.java`、`domain/dto/FollowCreateDTO.java`、`domain/entity/CustomerFollow.java`(出参=实体直出) - **page 请求参数**(GET query):`id`(必填)+ `followWay`(string,follow_way 字典 code 精确,例 `follow_way_01`)+ `startTime`/`endTime`(string,yyyy-MM-dd HH:mm:ss 含端)+ BaseParam 五参。 - **G1 标注**:原型有「操作人」筛选,`FollowPageParam` **无 operatorUserId 参数**——后端无此过滤能力,demo 侧豁免该筛选(P2-G1 记档)。 - **page 出参**(`PageResult`,行 13 字段,实体直出含审计四件套): | 字段路径 | 类型 | 口径 | 示例 | |---|---|---|---| | content[].id / creatorId / createTime / updaterId / updateTime / deleted | — | BaseEntity 审计字段(E2E 实测序列化;`"deleted":false`) | `"750854396027338752"` | | content[].customerId | Long→str | 所属客户 ID | `"750844477165273088"` | | content[].followWay | string | follow_way 字典 code(5 值见字典节;**后端不回名**) | `"follow_way_01"` | | content[].followContent | string | 跟进内容(长文本) | `"e2c-F08 跟进实测:电话沟通年度合作意向"` | | content[].nextFollowTime | string | 下次跟进时间(有值进「待跟进」筛选) | `"2026-09-10 23:07:18"` | | content[].followBy | Long→str | 跟进人用户 ID | `"739564171091247104"` | | content[].followByName | string | 跟进人姓名(服务端快照,前端不传) | `"罗伟健"` | | content[].followDeptName | string | 跟进人部门名 | `"广东保伦电子股份有限公司"` | - **add 请求参数**(form):`followWay`(必填,5 值域)、`followContent`(必填,空→67009)、`nextFollowTime`(选填,**ISO `T` 分隔** `2026-09-10T10:00:00`;空格分隔报 400;有值进待跟进)。 - **add 出参**:`Result` 跟进 id(E2E `"753583108837605376"`)。 - 备注:append-only 提交后不可改删(纠错=新增补充);同客户同内容 30 秒窗口防重复提交;写库刷新 `last_valid_follow_time` 超期锚点(旧未发提醒自动置已失效)。错误码 67002/67009。已知缺陷 D-02(P2):nextFollowTime 早于当前未拦截(.bru docs 记档)。 ### #45 操作日志分页 — `GET /api/customer/oplog/page?id=` - 出处:`A4 客户管理\客户详情\操作日志\操作日志分页.bru` + `domain/param/OplogPageParam.java`、`domain/entity/CustomerOplog.java`(出参=实体直出) - **请求参数**(GET query):`id`(必填)+ `startTime`/`endTime`(yyyy-MM-dd HH:mm:ss 含端)+ `action`(string,操作类型精确)+ BaseParam 五参(keyword 匹配 `detail` 叙事)。 - **action 值域**:权威=`CustomerConstants` 全部 ACTION_* 常量,**18 种**:`CREATE / UPDATE / FOLLOW / ARCHIVE / RESTORE / TRANSFER / CLAIM / POOL / STAGE_CHANGE / MEMBER_ADD / MEMBER_REMOVE / CONTACT_ADD / CONTACT_EDIT / CONTACT_DELETE / AGREEMENT_ADD / AGREEMENT_EDIT / AGREEMENT_DELETE / GRAPH_EDIT`。(漂移标注:AS §3 写「17 种」未含 GRAPH_EDIT,未同步;demo 筛选下拉按 18 种渲染。) - **出参**(`PageResult`,行 12 字段): | 字段路径 | 类型 | 口径 | 示例 | |---|---|---|---| | content[].id / creatorId / createTime / updaterId / updateTime / deleted | — | BaseEntity 审计字段 | `"750854380575522816"` | | content[].customerId | Long→str | 所属客户 ID | `"750844477165273088"` | | content[].action | string | 18 种值域(见上) | `"UPDATE"` | | content[].detail | string | 叙事形态 `把 {字段} 从 {旧} 修改为 {新}`(D13;多字段变更串联一条) | `"把人脉关系单位从[...]修改为空;把公司决策人从王总修改为空;…"` | | content[].operatorId | Long→str | 操作人用户 ID(**系统动作为 null**,如阶段自动流转/导入) | `"739564171091247104"` | | content[].operatorName | string | 操作人姓名(系统动作记「系统」) | `"罗伟健"` | | content[].operatorDeptName | string | 操作人部门名快照 | `"广东保伦电子股份有限公司"` | - 备注:append-only 审计、无限期保留;时间倒序;日期分组由前端渲染。错误码 67002。导出按钮无端点=豁免(P3-4)。 ### #41 关联商机页签 — `GET /api/customer/opportunity/page?id=`(port 反查 crm-opportunity) - 出处:`A4 客户管理\客户详情\关联商机\关联商机页签.bru` + `port/CustomerOpportunityQueryPort.java`(record OpportunityItem,7 字段真名逐字如下) - 请求参数:`id`(必填)+ BaseParam 五参。 - 出参(`PageResult`,**契约 7 字段**;G2:原型 13 列 vs 契约 7 字段,demo 现 5 列): | 字段路径 | 类型 | 口径 | 示例 | |---|---|---|---| | content[].id | Long→str | 商机 ID | `"750844498195513344"` | | content[].oppName | string | 商机名称 | `"e2c-联动-交割"` | | content[].stageName | string | 商机阶段名(已回显名) | `"客户圈定"` | | content[].estimateAmount | BigDecimal | 预计金额 | `null` | | content[].ownerUserId | Long→str | 销售负责人 ID | `"739564171091247104"` | | content[].ownerNameSnapshot | string | 负责人姓名快照(可 null→"--") | `null` | | content[].updateTime | string | 最近更新时间 | `"2026-09-03 22:27:58"` | - 备注:port 失败前端显 "--" 不显 0(issues-07 §A);列清单以 03 票实现实返为准收口 G2。错误码 67002。 ### #43 战略协议五端点 — `CustomerAgreementController /api/customer/agreement/*` - 出处:`A4 客户管理\战略协议\*.bru` 五件 + `domain/dto/AgreementDTO.java`(ADR-0017 Route A:写入参/出参双向同类) - **AgreementDTO 字段**(双向): | 字段 | 类型 | 必填(写) | 口径 | |---|---|---|---| | customerId | Long→str | 新增必填;**编辑传入忽略**(归属不可换客户,以库内为准) | 所属客户 | | agreementLevel | number | 是 | 1~3=一级/二级/三级;越界 67017 | | amount | string | 否 | 签订金额**文本**(数字/区间/含单位,E2E `"500万"`、`"128000"`) | | remark | string | 否 | 备注 | | fileFileId | string | 否 | 附件 fileId(crm-file 引用字符串,单文件可空);**demo 以纯文本框呈现**(附件上传位豁免 D2),下载由前端走 crm-file 通用端点 | | customerName | string | 仅出参 | 所属客户名称(卡片带出) | | id / createTime / updateTime | — | 仅出参 | BaseDTO(E2E detail 实测有值) | | 端点 | 参数 | 出参 data | 错误码 | |---|---|---|---| | `GET /api/customer/agreement/list?customerId=` | customerId | `List`(页签卡片,时间倒序**非分页**;E2E 空列表 `[]`) | 67002 | | `GET /api/customer/agreement/detail?id=` | id | `AgreementDTO` 全量 | 67017/67002 | | `POST /api/customer/agreement/create`(form) | AgreementDTO | `Long` 协议 id | 67001/67017/67002 | | `POST /api/customer/agreement/edit?id=`(form) | id + AgreementDTO | `null` | 67001/67017/67002 | | `POST /api/customer/agreement/delete?id=` | id | `null`(软删) | 67002/67017 | - 备注(**快照联动**):新增/编辑/删除协议同步刷新客户主表 `strategic_agreement_level`(删除=删空后快照回 NULL=未签);oplog 记 `AGREEMENT_ADD/EDIT/DELETE`;67017 语义=等级缺失或越界/协议不存在或无权访问。权限继承所属客户(无独立 ACL,无权 67002/67017)。 ### #44 团队成员三端点 — `GET member/list?id=` + `POST member/add?id=` + `POST member/remove?id=&memberUserId=` - 出处:`A4 客户管理\客户详情\团队成员\*.bru` 三件 + `domain/dto/CustomerMemberDTO.java`、`MemberAddDTO.java` - `GET /api/customer/member/list?id=` 出参(`List`,**data 内仅 4 字段**;负责人 ROLE_OWNER 在前): | 字段路径 | 类型 | 口径 | 示例 | |---|---|---|---| | [].userId | Long→str | 成员用户 ID | `"739564171091247104"` | | [].userName | string | 姓名(负责人=主表快照,协同人=团队成员快照) | `"罗伟健"` | | [].deptName | string | 部门名(负责人=主表快照,协同人=**实时回显**) | `"广东保伦电子股份有限公司"` | | [].role | string | 角色枚举:`OWNER` 负责人 / `COLLABORATOR` 协同人(**字段名是 role,不是 memberRole**) | `"OWNER"` | - **G3 标注**:原型另有职务/加入时间两列——`memberRole`、`createTime` **均不在此 DTO**(不暴露),demo 恒 "--" 渲染=记档口径(P2-G3,字段级挂 07 票)。 - `POST /api/customer/member/add?id=`:form 绑定 `MemberAddDTO{memberUserIds: List}`(**重复键** `memberUserIds=..&memberUserIds=..`,示例 `744842318024015872`)→ `null`。两阶段校验**整批拒绝**:名单空/含销售负责人/重复加入/用户停用→67016;批内幂等去重;姓名快照;oplog `MEMBER_ADD`。E2E 反例:`{"code":67016,"message":"成员名单不能为空"}`。 - `POST /api/customer/member/remove?id=&memberUserId=`:查询参数两件 → `null`。软删保留历史(delete_key 复用键,可重复加入);oplog `MEMBER_REMOVE`;错误码 67002/67016。 ### #42 关联项目页签 — 占位(demo 无端点调用,内容恒 "--") - 矩阵 #42 口径:**demo 侧无入口调用、页签占位显 "--"**(「新增占位页签」动作)。漂移提示:契约面实际已存在 `GET /api/customer/project/page?id=`(`CustomerDetailController.pageProjects`,`CustomerProjectQueryPort.ProjectItem` 9 字段),且 `A4 客户管理\客户详情\关联项目\关联项目页签.bru` 已有 E2E 真值(projectName=e2c-r3d-n3-项目甲、stageName=冲突处理、statusName=进行中、filingStatusName=报备审核中)——AS §5.3「A5 未建恒 --」记档已过时;demo 维持占位,切真值属后续票,本表不展开。 --- ## 字典与枚举(值域 + 文案 + 出处) | 字典/枚举 | 值域与文案 | 出处 | |---|---|---| | customer_stage | 1 潜在客户 / 2 重潜客户 / 3 已成交客户(三态只前进,CAS;**本期两个跃迁均不触发,实际只有「潜在」**,D20 seam) | `domain/enums/CustomerStageEnum.java`;detail-head `customerStageName` 回显「潜在客户」;AS §3 | | customer_star_level | 1~5 整数,无文案字典(星级组件渲染) | DTO @Schema;AS §2.1 | | relation_star_level | 1~5 整数(导入 INSERT 缺省补 0=未评估占位,D-05 修复口径);看板 groupColumn=`relation` 分组 1~5 | AS §5.7/§2.1 | | customer_type | `customer_type_01` 总包 / `_02` 工程商 / `_03` 投资方 / `_04` 设计院 / `_05` 集成商 / `_06` 投标公司 | `crm-dict/.../config/DictDataInitializer.java` L325-332 | | industryCode | 运行时字典接口/字典树核对(industry 分组两级行业树,客户用二级/商机用一级);E2E 样例 `gov`=政府机关、`gov_1`=人大政协 | DictDataInitializer L95;E2E detail 实测 | | follow_way | `follow_way_01` 电话沟通 / `_02` 上门拜访 / `_03` 微信对接 / `_04` 线上会议 / `_05` 展会沟通 | DictDataInitializer L293-299;FollowCreateDTO @Schema | | viewType(内置五视图) | `ASSIGNED` 我负责的 / `COLLABORATING` 我协同的 / `FOLLOW_UP_DUE` 待跟进 / `FOCUSED` 我关注的 / `RECENT` 最近访问;适配矩阵见 #1 | AS §2.1/§3 | | archiveStatus | 1 有效 / 2 已归档(无合并态,D19 归档=删除语义) | AS §3 | | view-form | `list` / `split` / `board`(白名单外 68001;pool 拒 board 文档口径未实现=I-06 漂移) | ViewFormController/Impl;.bru 保存视图形态.bru;AS §2.14 | | oplog action | 18 种(含 `GRAPH_EDIT`),逐值见 #45;AS §3「17 种」为未同步漂移 | `CustomerConstants` L97-114 | | BatchFailReason | `NOT_FOUND / STATUS_NOT_ALLOWED / CONCURRENT_MODIFIED / ARCHIVE_BLOCKED / POOL_BLOCKED / ASSIGN_INVALID / UNKNOWN` | `domain/enums/CustomerBatchFailReason.java` | | member role | `OWNER` / `COLLABORATOR` | CustomerMemberDTO 常量 | | transferReason(交割,本表外引用) | `resign` 离职 / `transfer_post` 岗位调动 / `region_adjust` 区域调整 | AS §3 | | job_title | 运行时两级字典树(demo 动态拉取),无静态值域,不落本表 | AS §2.7(jobTitleId/Name/Category/Level 联动 D1/D2) | --- ## 来源文件清单 **Bruno 仓 `D:\code\crm-api-docs\`**(相对根): - `A4 客户管理\我的客户\workspace 列表分页.bru`、`新增客户.bru`、`快速创建客户.bru`、`编辑客户.bru`(客户详情\)、`工商信息查询.bru` - `A4 客户管理\客户总览\workspace 列表分页.bru`、`看板列表\看板分组汇总.bru`、`看板单列卡片.bru`(我的客户同名) - `A4 客户管理\客户查重\名称相似提示.bru`、`信用代码查重.bru` - `A4 客户管理\客户详情\详情公共头部.bru`、`客户详情.bru`、`标记重点客户.bru`、`取消重点标记.bru`、`跟进记录\跟进记录分页.bru`、`跟进记录\新增跟进.bru`、`操作日志\操作日志分页.bru`、`关联商机\关联商机页签.bru`、`关联项目\关联项目页签.bru`、`团队成员\团队成员页签.bru`、`团队成员\添加团队成员.bru`、`团队成员\移除团队成员.bru` - `A4 客户管理\客户公海\关注客户.bru`、`取消关注客户.bru`、`批量关注客户.bru`、`领取客户.bru`、`批量领取.bru`、`分配客户.bru`、`批量分配.bru`、`抛公海.bru`、`批量抛公海.bru`、`归档客户.bru`、`批量归档.bru`、`恢复客户.bru`、`读取视图形态.bru`、`保存视图形态.bru`(我的客户/客户总览同名) - `A4 客户管理\战略协议\战略协议页签列表.bru`、`战略协议详情.bru`、`新增战略协议.bru`、`编辑战略协议.bru`、`删除战略协议.bru` - `A3 商机管理\销售机会\自定义视图\自定义视图列表.bru`、`保存自定义视图.bru`(saved-view 形态佐证,A4 无同名 .bru) **后端源码 `D:\code\crm-backend-matt\`**(绝对路径根同上): - `crm-customer\src\main\java\com\crm\customer\domain\dto\`:CustomerListRowDTO、CustomerBoardSummaryDTO、CustomerCreateDTO、CustomerUpdateDTO、CustomerQuickCreateDTO、CustomerSaveResultDTO、CustomerSimilarHitDTO、CreditCodeCheckDTO、CustomerSearchItemDTO、CustomerDetailHeadDTO、CustomerDetailDTO、FollowCreateDTO、AgreementDTO、CustomerMemberDTO、MemberAddDTO、CustomerBatchFailItem - `crm-customer\...\domain\param\`:CustomerWorkspacePageParam、CustomerSearchParam、FollowPageParam、OplogPageParam - `crm-customer\...\domain\enums\`:CustomerStageEnum、CustomerBatchFailReason;`constant\CustomerConstants.java` - `crm-customer\...\port\`:CustomerOpportunityQueryPort、CompanyLookupPort;`mapper\CustomerMapper.java`(pageWorkspace SQL:opportunityCount 真算/projectCount 恒 0/focusFlag/lastViewTime);`service\impl\CustomerWorkspaceServiceImpl.java`(board 守卫/排序/viewType);`controller\`:CustomerWorkspaceController、CustomerOwnershipController、CustomerFocusController、CustomerController、CustomerDetailController、CustomerAgreementController - `crm-preference\src\main\java\com\crm\preference\`:controller\SavedViewController、controller\ViewFormController、domain\dto\SavedView、domain\dto\SavedViewCondition - `crm-base\src\main\java\com\crm\base\domain\`:param\BaseParam、dto\BaseDTO、result\PageResult、result\BatchResult、result\Result - `crm-dict\src\main\java\com\crm\dict\config\DictDataInitializer.java`(customer_type/follow_way 种子) **仓内文档**: - `.scratch\customer-module\API-SUMMARY.md`(§0/§1/§2.1-2.6/§2.9/§2.14/§3/§5) - `.scratch\customer-demo-ref\assets\coverage-matrix.md`(入口编号/G1-G6/P3-x 依据) - `.scratch\customer-frontend-handover\mapping-a4-pages.md`(原型列文案) - `.scratch\customer-e2e\demo\index.html`(fork 基线渲染口径:`|| '--'`、maskPhone/reveal、旧 loadSummary 读 count/name 的漂移证据) --- # 第二部分 · 联系人族 + 重流程 + 规则族(矩阵 #31-#69) > 用途:客户单文件 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`