You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
192 lines
14 KiB
192 lines
14 KiB
|
9 hours ago
|
# 客户联系人图谱与联系人页签整节 spec(冻结版)
|
||
|
|
|
||
|
|
created: 2026-09-10(票 01 grilling 终稿,12 项用户拍板 + 2 项技术自定)
|
||
|
|
上游:[票 00 原型 demo](issues/00-prototype-demo.md)(活参照,7 轮验收修订)+ 需求权威 `.scratch/opportunity-bugfix/lanhu-pages/A4-4-2_客户详情(联系人).md`(u533 列表侧 / u536 图谱 15 条)
|
||
|
|
下游:02~07 全部实现票的**唯一输入**;与原型冲突时以本 spec 为准(spec 冻结晚于原型验收)。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 一、决策全景(D1–D12 用户拍板 / T1–T2 技术自定)
|
||
|
|
|
||
|
|
| # | 决策点 | 拍板 |
|
||
|
|
|---|---|---|
|
||
|
|
| D1 | 等级真值 | **双值模型**:字典档位 = 等级**初始值来源**;图谱拖格子定级只改当前客户的联系人等级,**永不回写字典** |
|
||
|
|
| D2 | 职务变更流转 | **来源区分**:等级来自字典(未手工定级)→ 职务变更按新职务档位重算;**手工定级过 → 锁定不动**,职务变更不覆盖。加 `level_source` 标记列实现 |
|
||
|
|
| D3 | 分档表 | **三档制 10/6/3 原型转正**:董事长/股东=10,采购·市场·人事经理=6,采购·市场专员与投标商务=3,「其他」无档(落右侧栏)。字典档位仅作初始值 |
|
||
|
|
| D4 | 保存模型 | **保存才入库**:编辑态全部变更(连线增删/定级/撤级)驻前端 pending,单一 save 端点事务落库;重置 = 前端丢弃。后端全量校验 |
|
||
|
|
| D5 | 位移持久化 | **不持久化**:布局纯由 level 推导(y=等级行常量,x=行内稳定序),零坐标列 |
|
||
|
|
| D6 | 边模型 | **只有手动边,没有自动边**——推翻原型「紧邻更高档」自动生成机制(u536 第 2 条)与删除否决机制(第 9 条);所有上下级连线由用户手动拉出;边表 `source` 列砍掉 |
|
||
|
|
| D7 | 公司边 | **等级 10 自动挂公司 = 唯一保留的自动规则**(u536 第 7 条) |
|
||
|
|
| D8 | 公司边语义 | **派生视图,不可删**:不落库,读端点/前端按 level==10 实时算出;摘线 = 把节点拖离等级 10 格子(降级);豁免所有锁定约束(不算上级边也不算下级边,否则等级 10 节点被不可删的边永久锁死) |
|
||
|
|
| D9 | 并发冲突 | **独立图谱版本号**:`customer_contact_graph` meta 表(customer_id PK + version),save 乐观锁比对;页内编辑改职务致 level 跟随时也 bump version |
|
||
|
|
| D10 | 明文埋点 | **独立表** `customer_contact_reveal_log`(crm-customer 域内,结构化联系人/客户维度);采集失败自吞不阻塞明文返回(OpLogPort 纪律) |
|
||
|
|
| D11 | 联系人导入 | **独立表镜像形态**:`contact_import_task` / `contact_import_fail` 镜像 customer_import 全套(三段式/Analyzer 预检/fail 明细/clip500),Executor 骨架拍平复用 |
|
||
|
|
| D12 | contact_source | **四项含其他**:group_meeting(组会)/group_activity(组局)/referral(转介绍)/other(其他),与既有列注释一致;W-01 销号 |
|
||
|
|
| T1 | 错误码段 | 沿用 crm-customer **67xxx**(已用至 67016),从 **67017** 起(号位见 §五) |
|
||
|
|
| T2 | 规模上限 | 记录性预案(见 §六),票 03 读端点 + 票 08 demo 双验证,不提前实现 |
|
||
|
|
|
||
|
|
**作废清单**(spec 冻结即从原型行为中移除,demo 修订⑧回写,见 §七):自动生成边算法、重建触发(rebuildAutoEdges)、删除否决/tombstone、auto 多候选歧义规则(待钉点④随之作废)、边表 source 列。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 二、数据模型
|
||
|
|
|
||
|
|
### 2.1 既有表变更:`customer_contact`
|
||
|
|
|
||
|
|
| 变更 | 形态 |
|
||
|
|
|---|---|
|
||
|
|
| 新增列 `level_source` | `tinyint not null default 1 comment '等级来源:1=字典跟随 2=手工锁定'`(D2;手工拖格子定级后置 2,此后职务变更不覆盖 job_title_level;撤级清空 level 时一并回到 1) |
|
||
|
|
| `job_title_level` 语义 | 当前生效等级真值(1–10 或 null);null = 未设置等级(右侧栏)。已有四列留位(D5 遗产)直接启用 |
|
||
|
|
|
||
|
|
孤立单大写字母检查:`levelSource` / `jobTitleLevel` 均无孤立单大写,无需显式 @Column name。
|
||
|
|
|
||
|
|
### 2.2 新表:`customer_contact_edge`(手动边,唯一边存储)
|
||
|
|
|
||
|
|
```
|
||
|
|
id bigint PK
|
||
|
|
customer_id bigint not null -- 边所属客户(冗余,免 join 校验)
|
||
|
|
parent_id bigint not null -- 上级联系人 id(level 高者)
|
||
|
|
child_id bigint not null -- 下级联系人 id(level 低者)
|
||
|
|
+ BaseEntity(deleted 软删 / create_time / …)
|
||
|
|
唯一索引 uk_edge_pair (customer_id, parent_id, child_id, delete_key) -- 两点一条边兜底
|
||
|
|
索引 idx_edge_customer (customer_id)
|
||
|
|
```
|
||
|
|
|
||
|
|
约束(save 事务内全量校验):每 child 至多一条 parent 边(**单父**,公司节点豁免条款随 D8 派生化而无需存储层特例);无自连;无环(构造上 parentLevel > childLevel 严格递减保证,DFS 兜底);parent/child 必须同属该 customer 且 level 均非空。
|
||
|
|
|
||
|
|
### 2.3 新表:`customer_contact_graph`(图谱版本 meta,D9)
|
||
|
|
|
||
|
|
```
|
||
|
|
customer_id bigint PK -- 一客户一行
|
||
|
|
version int not null default 0 -- 乐观锁版本
|
||
|
|
+ BaseEntity
|
||
|
|
```
|
||
|
|
|
||
|
|
首次读时懒建行;save 比对 version 不符 → 67017 冲突;save 成功 / 页内编辑 level 跟随 / 联系人增删(影响图谱节点集)均 bump version。
|
||
|
|
|
||
|
|
### 2.4 新表:`customer_contact_reveal_log`(明文查看埋点,D10)
|
||
|
|
|
||
|
|
```
|
||
|
|
id bigint PK
|
||
|
|
customer_id bigint not null
|
||
|
|
contact_id bigint not null
|
||
|
|
operator_id varchar(64) not null -- 操作人用户 id(快照语义对齐 crm-log)
|
||
|
|
operator_name varchar(64) -- 姓名快照(可空,补全失败时)
|
||
|
|
op_time datetime not null
|
||
|
|
ip varchar(64)
|
||
|
|
索引 idx_reveal_contact (contact_id, op_time) / idx_reveal_time (op_time)
|
||
|
|
```
|
||
|
|
|
||
|
|
只写不管理(无查询页);e2e 直查表断言。
|
||
|
|
|
||
|
|
### 2.5 新表:`contact_import_task` / `contact_import_fail`(D11,镜像 customer_import 形态)
|
||
|
|
|
||
|
|
```
|
||
|
|
contact_import_task:customer_id(导入目标客户,任务语义与客户导入的差异点)+ import_mode
|
||
|
|
(APPEND_ONLY/UPDATE_ONLY/UPSERT,查重键=客户内手机号)+ status/total/success/fail
|
||
|
|
/file_id(模板留档)/creator_id/finish_time —— 其余列镜像 customer_import_task
|
||
|
|
contact_import_fail:task_id + row_num + name + phone + fail_reason(镜像 customer_import_fail)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.6 crm-dict 缝合(票 02 落地,需 ADR + crm-dict CONTEXT.md 双文档化)
|
||
|
|
|
||
|
|
| 变更 | 形态 |
|
||
|
|
|---|---|
|
||
|
|
| `dict_item` 新增列 `level` | `int comment '档位 1–10(仅 job_title 组职务项使用,空=无档)'`;其他组不用留空 |
|
||
|
|
| 种子组 `job_title`(两级树,复用行业 parentId 先例) | 一级类别 5 项:executive(高层管理)/purchase(采购)/marketing(市场)/hr(人事)/other(其他);二级职务 9 项:chairman(董事长,10)/shareholder(股东,10)/purchase_manager(采购经理,6)/purchase_specialist(采购专员,3)/marketing_manager(市场经理,6)/marketing_specialist(市场专员,3)/hr_manager(人事经理,6)/bid_officer(投标商务,3)/other_job(其他职务,无档)——其他类别下 2 项(投标商务/其他职务),其余各类别 1–2 项;二级兑底 code 用 other_job 避免与一级 other 撞组内 code/name 唯一性 |
|
||
|
|
| 种子组 `contact_source` | group_meeting(组会)/group_activity(组局)/referral(转介绍)/other(其他),全部无档位 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 三、端点契约(ADR-0017 纪律:flat 动词 + 查询参数传 id;写 POST / 读 GET;隐式表单绑定,禁 @PathVariable/PUT/DELETE/@RequestBody;深 tag 一级 = A4 客户管理)
|
||
|
|
|
||
|
|
### 3.1 图谱组(A4-4-2 §6.4,票 03)
|
||
|
|
|
||
|
|
| 端点 | Method | 入参 | 出参 / 错误 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `/api/customer/contact/graph/detail` | GET | customerId | `GraphDTO{version, nodes[{contactId,name,jobTitleName,jobTitleCategory,level,levelSource,phoneMasked,isKeyContact}], edges[{parentId,childId}]}`——公司边**不含**(前端按 level==10 派生渲染,D8);越权客户 → 67002 |
|
||
|
|
| `/api/customer/contact/graph/save` | POST | customerId, version, `nodes[i].contactId`, `nodes[i].level`(全量快照,level 可空=撤级), `edges[i].parentId`, `edges[i].childId`(全量最终边集) | `Result<Long>`(新 version);version 不符 → **67017**;边非法(自连/重复/两点多条/等级倒挂/越界联系人)→ **67018**;单父违反/环 → **67019** |
|
||
|
|
|
||
|
|
save 语义:全量快照 diff(后端按 customer_id 比对现库边集,增删落库;level 变更集批写 customer_contact 并维护 level_source;联系人被删过的边级联软删);成功后 version+1。
|
||
|
|
|
||
|
|
### 3.2 列表侧组(§6.1/6.2/6.3,票 04)
|
||
|
|
|
||
|
|
| 端点 | Method | 入参 | 出参 / 错误 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `/api/customer/contact/reveal` | GET | id(contactId) | 明文 phone +(记 reveal_log,失败自吞不阻塞);联系人不存在/越权 → 67008 |
|
||
|
|
| `/api/customer/contact/quickAdd` | POST | customerId, `rows[i].name/jobTitleCode/phone/source/isKeyContact` | 批量结果(成功数+失败明细行);行级非法(姓名/职务缺失、手机号格式)→ **67020** |
|
||
|
|
| `/api/customer/contact/batchEdit` | POST | `rows[i].id` + 变更字段(name/jobTitleCode/phone/source/isKeyContact) | 批量结果;职务变更按 D2 规则联动 level(字典来源重算 + bump graph version);行级非法 → **67021**;越权 → 67008 |
|
||
|
|
| `/api/customer/contact/export` | POST | customerId(全量导出该客户联系人) | 走 BatchOpLog + MinIO 结果文件留档(LogBatch 先例,票 04 对齐 LeadPool 导出实现) |
|
||
|
|
| 既有列表/详情端点 | — | — | phone 出参一律脱敏(138\*\*\*\*1234);等级徽标列 = job_title_level 回显 |
|
||
|
|
|
||
|
|
### 3.3 联系人导入组(§6.5,票 05,镜像客户导入六端点)
|
||
|
|
|
||
|
|
| 端点 | Method | 入参 | 出参 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `/api/customer/contact/import/template` | GET | — | byte[] xlsx(列:姓名*、职务(字典 code)、手机号*、来源、关键联系人) |
|
||
|
|
| `/api/customer/contact/import/upload` | POST | file(multipart), customerId, importMode, duplicateStrategy? | `ImportPreviewDTO`(预检明细 + fail 行);预检失败 → **67022** |
|
||
|
|
| `/api/customer/contact/import/confirm` | POST | taskId | task id(DRAFT→RUNNING 异步执行,对齐客户导入) |
|
||
|
|
| `/api/customer/contact/import/result` | GET | taskId | `ImportResultDTO`;状态不允许 → **67023** |
|
||
|
|
| `/api/customer/contact/import/failures` | GET | taskId | `List<ImportFailDTO>` |
|
||
|
|
| `/api/customer/contact/import/page` | GET | ImportPageParam(keyword/status 分页) | `PageResult<ImportResultDTO>` |
|
||
|
|
|
||
|
|
Analyzer 预检清单(D-05/D-04 教训前移):NOT NULL 无默认列必填校验、手机号 11 位格式、职务 code 存在于 job_title 字典、来源 code 存在于 contact_source 字典、客户内手机号查重(按 importMode/duplicateStrategy 判定)、行数 clip500。导入落库的联系人 level 按 D1 初始值填充(字典档位),level_source=1。
|
||
|
|
|
||
|
|
### 3.4 字典组(票 02,零新端点)
|
||
|
|
|
||
|
|
- 职务二级级联:复用 `GET /api/dict/item/tree?groupCode=job_title`(行业两级树先例,DictItemDTO 增 level 回显);选择器落库写 customer_contact 四列(job_title_id/name/category/level)。
|
||
|
|
- 来源下拉:复用 `GET /api/dict/item/enabled-list?groupCode=contact_source`。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 四、校验与规则快照(后端 save 全量校验清单)
|
||
|
|
|
||
|
|
1. 单父:全量快照中每个 childId 至多出现一次(公司节点不在快照内,天然豁免)。
|
||
|
|
2. 两点一条边:(customer, parent, child) 快照内查重 + DB 唯一索引兜底。
|
||
|
|
3. 无环:构造上边要求 parentLevel > childLevel(等级严格递减 → 有向无环);save 校验每条边等级关系成立(未定级节点不得作为边端点),DFS 检测降为兜底断言。
|
||
|
|
4. 等级约束(修订⑦口径):**有上级连线不能调级;只有下级连线可调但新等级须高于最高下级;撤级须无任何边**——后端按全量快照终态校验(前端编辑态即时提示同口径)。
|
||
|
|
5. level ∈ {null, 1..10};contactId 必须属于 customerId 客户。
|
||
|
|
6. 事务边界:save 单事务(diff + level 批写 + version CAS +1);任一校验失败整体回滚。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 五、错误码分配(T1:67xxx 从 67017 起)
|
||
|
|
|
||
|
|
| 码 | 语义 |
|
||
|
|
|---|---|
|
||
|
|
| 67017 | 图谱版本冲突(save 乐观锁;提示刷新后重试) |
|
||
|
|
| 67018 | 图谱边非法(自连/重复/两点多条/等级倒挂/端点越界) |
|
||
|
|
| 67019 | 图谱结构校验失败(单父违反/环) |
|
||
|
|
| 67020 | 联系人快速添加行级非法 |
|
||
|
|
| 67021 | 联系人页内编辑行级非法 |
|
||
|
|
| 67022 | 联系人导入预检失败(文件/表头/行数/字典 code/查重策略) |
|
||
|
|
| 67023 | 联系人导入任务状态不允许 |
|
||
|
|
|
||
|
|
号位实现期可顺延微调,段位 67xxx 不变。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 六、规模上限预案(T2,记录性)
|
||
|
|
|
||
|
|
- 设计基线:单客户 ≤500 联系人、≤1000 边。图全量读端点一次返回(数百节点 JSON < 1MB),demo 前端绝对定位卡片 + SVG 连线渲染可行(票 08 验证)。
|
||
|
|
- 超限形态:10 个等级行 × 50+ 列的水平挤压——预案 = 行内虚拟滚动或卡片收缩视图,**不在本期实现**;票 03 读端点预留 nodes 数组结构不变,票 08 用 500 节点 mock 压测 demo 渲染。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 七、demo 修订⑧(spec 冻结行为对齐,票 06 前置)
|
||
|
|
|
||
|
|
原型 prototype-contact-graph.html 与本 spec 的行为差异,票 06 接真实 API 前回写(直改 prototype/ 续记修订⑧):
|
||
|
|
|
||
|
|
1. 砍自动生成边:删 rebuildAutoEdges/autoParentsOf 与等级 100→10 残留逻辑,初始边全部改手动(mock 数据同)。
|
||
|
|
2. 公司边派生化:公司边不显示 ×(不可删),摘线=降级;等级锁定校验豁免公司边。
|
||
|
|
3. 无否决/tombstone 交互(随 auto 边一并消失)。
|
||
|
|
4. level 来源标记(字典跟随/手工锁定)在页内编辑-职务变更联动上体现(demo mock 可简化为提示文案)。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 八、交付物落点
|
||
|
|
|
||
|
|
- 本 spec = 02(字典前置)/ 03(图谱后端)/ 04(列表侧增强)/ 05(联系人导入)/ 06(demo 接 API)/ 07(E2E)的唯一契约输入。
|
||
|
|
- 票 02 产出:crm-dict 缝合 ADR + dict_item.level 列 + 两组种子 + CONTEXT.md 更新。
|
||
|
|
- 票 03/04/05 按 §二/§三 落 DDL(ddl-auto: update 建表,注意 BOM 与孤立单大写纪律)与端点。
|