# 06 — 联系人建模 CRUD + 全局搜索 + 商机侧硬依赖接口 Type: research Status: resolved Blocked by: 02 ## 问题 定 `customer_contact` 实体表、CRUD 接口、全局搜索(商机侧硬依赖)、联系人电话查重、职务字段占位(图谱划出但字段留占位)。归属客户不孤立、权限继承客户团队。 ## 一手证据(蓝湖缓存 txt) - `a4-3-3-1_新增客户信息.txt` — 新增客户内嵌联系人明细表(序号/*联系人/*职务/联系电话/是否内线/礼品备注/来源/操作),OCR/批量导入按钮划出 - `a4-5-1_联系人列表_列表视图_.txt` / `a4-5-1_联系人列表_分屏视图_.txt` — 顶级联系人菜单 - `a4-5-2_新增联系人.txt` / `a4-5-3_编辑联系人.txt` — 独立联系人 CRUD - `a4-5-4_联系人详情_联系人信息_.txt` / `a4-5-5_联系人详情_联系人信息_.txt` — 联系人详情 - `a4-5-5_联系人详情_操作日志_.txt` / `a4-5-6_联系人详情_操作日志_.txt` — 联系人操作日志 - `a4-3-4_添加联系人.txt` — 客户下快速添加联系人 - `a4-4-2_客户详情_联系人_.txt` — 客户详情联系人页签 - `.scratch/opp-customer-dependency/商机模块前置客户接口依赖分析报告.md` — 商机硬依赖清单 ## 已知字段线索 - **联系人**(必填)— 姓名。 - **职务**(必填)— 见 `a4-3-3-1.txt` 底部「职务 - 搜索二级联动下拉框」需求:**图谱划出但存储字段留占位**:`job_title_name`(文本必填) + `job_title_id`(标准职务id可空) + `job_title_category`(类别可空) + `job_title_level`(层级 int 可空)。自定义职务时后三列空。 - **联系电话** — 手机号格式;**全库按电话查重**:新增/编辑时电话完全相同禁止创建;同表格内不同行电话相同禁止提交。 - **是否内线** — 布尔。 - **是否关键联系人** — 布尔(原型有)。 - **礼品备注** — 文本。 - **来源** — 字典 `contact_source`:组会/组局/转介绍/其他。 - **归属客户** — `customer_id` NOT NULL(无孤立联系人)。 - **系统字段** — 创建人/时间、修改人/时间。 ## 商机侧硬依赖(对齐 opp-customer-dependency P0/P1) | 优先级 | 接口 | 商机侧用途 | |---|---|---| | P0 | `POST /api/customer/search` | 商机新增关联客户下拉数据源 | | P0 | `GET /api/customer/{id}/contacts` | 商机字段17按客户筛选联系人 | | P0 | `GET /api/customer/{id}` | 商机编辑回显客户带出 | | P1 | `GET /api/customer/contact/search` | 商机字段13全局搜联系人+带出所属公司 | | P1 | `POST /api/customer/quick-create` | 商机弹窗「+快速创建客户」入口 | **本票产出**:以上契约签名 + 请求/响应 DTO(复用已有的分析报告,把它 promote 成正式契约稿)。 ## 待拍板点 1. **联系人权限模型**:明说「继承客户团队权限」——本票落成契约:无独立 `@DataScope` 注解,服务层显式按 customer 校验(对客户有读权限即对全部联系人有读权限)。 2. **联系人删除**:软删还是硬删?→ 推荐软删(联系人可能被商机/项目引用做快照);主表加 `deleted` 位。 3. **联系人电话唯一 vs 允许一号多人**:原型「联系电话完全相同禁止新建」听起来是全库唯一——但同一联系人可能有座机 vs 手机?→ 拍板:**仅手机号做唯一约束**,座机不查重;或直接单一 `phone` 列全库唯一,用户共号自己处理。先按后者简化。 4. **快速创建客户 `POST /api/customer/quick-create`**:字段集是**新增客户表单的必填最小子集**?还是就用完整表单?→ 推荐最小集(客户名称/客户类型/所属地区/行业/星级/关系星级)+ 服务端默认(销售负责人=当前用户/is_child=否)。 5. **联系人的操作日志**:单独一张 `customer_contact_oplog` 还是并入客户 oplog(票 07)用 `biz_type=CONTACT`?→ 推荐后者,参商机 oplog 口径。 6. **人脉关系单位** 与联系人是独立集合,仅是标签(非联系人子表)——归客户主表 JSON/子表(票 02 决定)。 ## Answer **Status: resolved** ### A. 实体 `customer_contact`(crm-customer,独立表) | Java 字段 | 列名 | 类型 | 必填 | 说明 | |---|---|---|---|---| | id | id | bigint | | 主键 ASSIGN_ID | | customerId | customer_id | bigint | ✅ | 所属客户;NOT NULL,无孤立联系人 | | name | name | varchar(50) | ✅ | 联系人姓名 | | jobTitleName | job_title_name | varchar(100) | ✅ | 职务名称(文本,始终存) | | jobTitleId | job_title_id | bigint | | 标准职务 id(字典/预置表),自定义职务时为 NULL | | jobTitleCategory | job_title_category | varchar(50) | | 职务类别(高层管理/采购/…),自定义职务时为 NULL | | jobTitleLevel | job_title_level | int | | 职务层级(图谱用),自定义职务时为 NULL | | phone | phone | varchar(20) | | 联系电话;手机号格式校验 | | isKeyContact | is_key_contact | tinyint | | 是否关键联系人(是1/否0) | | isInternal | is_internal | tinyint | | 是否内线(是1/否0) | | giftRemark | gift_remark | varchar(500) | | 礼品备注 | | source | source | varchar(20) | | 来源字典 `contact_source`:组会/组局/转介绍/其他 | | createBy / createTime / updateBy / updateTime | — | — | | BaseEntity | | deleted | deleted | tinyint default 0 | | 逻辑删除 | > ⚠ **孤立单大写字母检查**(AGENTS.md):`isKeyContact`→`is_key_contact`、`isInternal`→`is_internal`、`jobTitleName`/`jobTitleId`/`jobTitleCategory`/`jobTitleLevel`——均为「完整词 + 完整词」,**无需 `@Column(name=...)`**。 **索引**: - `idx_contact_customer` (customer_id, deleted) - `uk_contact_phone` (phone) — **仅手机号唯一**;NULL 不参与(无电话时允许,票 04 B4 内联查重兜底) - `idx_contact_name` (name) — 全局搜索 **权限模型(定稿,原型明文「联系人不设独立 ACL,继承客户权限」)**: - 无独立 `@DataScope` 注解;服务层显式按 customer 校验(对客户有读权限 → 对其全部联系人有读权限)。 - 无客户读写权限时隐藏联系人新增/编辑/删除入口;服务端仍校验。 ### B. 契约(对齐商机硬依赖,promote 自 opp-customer-dependency 分析报告) | 优先级 | 接口 | 说明 | |---|---|---| | P0 | `POST /api/customer/search` | 客户搜索(名称/联系人/电话/类型 四维模糊 + 分页),商机新增关联客户数据源 | | P0 | `GET /api/customer/{id}/contacts` | 按客户查联系人列表 | | P0 | `GET /api/customer/{id}` | 客户基础信息带出 | | P1 | `GET /api/customer/contact/search` | 联系人全局搜索(带出所属公司名),商机「商机关键联系人」数据源 | | P1 | `POST /api/customer/quick-create` | 快速创建客户(最小必填集),商机弹窗入口 | | P2 | `GET /api/customer/{id}/detail` | 客户详情(新窗口跳转),复用票 07 详情页 | ### C. 联系人 CRUD 契约(定稿) ``` POST /api/customer/contact 新增联系人(可指定 customer_id 或独立新增后关联) PUT /api/customer/contact/{id} 编辑 DELETE /api/customer/contact/{id} 删除(软删) GET /api/customer/contact/{id} 详情 GET /api/customer/contact/page 顶级联系人列表(A4-5-1,跨客户,按 DataScope) POST /api/customer/contact/import 批量导入(划出本图 → 随图谱 effort,本图只留接口占位) ``` ### D. 随客户新增的内嵌明细(定稿) - 新增客户表单「联系人」Tab = 明细表数组,**客户保存成功后**与客户建立关联(原型 3.5 §1)。 - 电话内联查重:同表电话相同两行禁止提交;全库电话精确查重(票 04 B4)。 - 删除仅移除本次未保存行;已输入内容行删除前确认。 ### E. 待产品确认(⚠) | # | 问题 | 影响 | |---|---|---| | P0-1 | 联系人电话唯一约束:**手机号全局唯一** vs 允许一号多人(共号场景)?原型「完全相同禁止新建」倾向唯一——但座机分机号可能重复。**推荐:仅手机号(11位)唯一,座机/短号不做唯一**,`phone` 单列存但唯一索引带条件(`phone IS NOT NULL AND LENGTH(phone)=11` 或用生成列)。 | `uk_contact_phone` 实现 | | P1-1 | 「来源」字典 `contact_source` 完整值(组会/组局/转介绍/其他,原型明列 4 项,是否还要更多) | crm-dict 种子 | ### F. 回填 / 下游 - **票 02**:无主表字段变化(联系人是子表)。 - **票 04**:联系人内联查重(B4)已回填。 - **票 07**:客户详情「联系人」页签复用 `GET /api/customer/{id}/contacts`。 - **票 01**:CONTEXT.md 术语「联系人」「关键联系人」。 - **crm-dict**:新增 `contact_source` 分组 + `job_title` 标准职务预置(图谱 effort 用它,本图只留 jobTitleId 引用位)。