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.

142 lines
10 KiB

5 days ago
# 08 — 客户交割(跨模块同步商机/项目负责人)
Type: research
4 days ago
Status: resolved
5 days ago
Blocked by: 02, 03
## 问题
定客户交割全流程:发起交接 → 客户分配 → 交接记录 + 详情。核心难点是**跨模块负责人同步变更事务**(客户+关联商机+关联项目一起换主,要么全成要么全不成)。
## 一手证据(蓝湖缓存 txt)
- `a4-7-1_客户交割_发起交接_.txt` — 发起交接页(三 tab 之首)+ 交接原因/预览/确认弹窗 + **公共规则/字段规则/客户及关联业务变更/加载与操作反馈**(关键 spec 段)
- `a4-7-2_客户交割_客户分配_.txt` — 客户分配 tab(接收总监分配到具体销售)
- `a4-7-3_客户交割_交接记录_.txt` — 交接记录 tab
- `a4-7-4_交接记录详情.txt` — 交接记录详情
## 已知事实(关键提炼)
- **三 tab**:发起交接 / 客户分配 / 交接记录(+ 记录详情)。
- **交接原因**:员工离职 / 岗位调动 / 区域调整(单选必选)。
- **发起人**:仅销售**本人**可发起本人名下客户交接(不允许主管/管理员代发起)。
- **接收人**:仅交接人员的**直属销售总监**;员工离职时系统自动定唯一总监,岗位调动/区域调整时手选启用的直属总监。
- **可交接客户范围**:交接人员**当前为唯一负责人**的**未删除、未进入公海**客户;仅协同人身份的不纳入。
- **员工离职**:必须交接**全部可交接客户**(无勾选框);**岗位调动/区域调整**:至少勾选1个,跨分页勾选。
- **发起交接后同步变更**:客户负责人 → 接收总监;**该客户全部关联商机 + 项目负责人同步变更为同一接收人**;协同人保留;商机/项目其他数据不变。
- **原子性**:客户+商机+项目负责人**全部成功**才算本客户处理成功;不允许出现「客户变了但关联商机未变」的中间态;任一失败整批不生效、不生成正式交接记录、当前表单/搜索条件/勾选保留。
- **客户分配 tab**:接收总监按交接批次把客户分配给具体销售(单条/批量);分配对象排除交接来源人本人;同步变更客户+关联商机+项目负责人;成功后原负责人(接收总监)移出团队。
- **未完成交接约束**:同一交接人员有未完成交接记录时不允许再次发起。
- **交接记录**:唯一交接编号;初始分配状态=待分配,进度=0/N;随分配推进;跟操作日志区分(跨模块自动变更的操作人显示"系统"+注明交接编号)。
## 待拍板点
1. **跨模块负责人同步的事务边界**
- **推荐 A(同步事务)**:本地事务包含 customer 表 + 调用 crm-opportunity 的 `batchTransferOwner(oppIds, newOwner)` + 调用 crm-project 的同名接口;任一失败整批回滚。但 crm-project 未建 → 本图只做客户+商机两侧同步,项目侧留 seam(`ProjectOwnerTransferPort`)。
- **推荐 B(异步 saga)**:客户先变,商机/项目通过消息异步跟;失败重试;对账。
- 我倾向 **A(同步事务)**:原型明说「不允许出现中间态」→ saga 语义不匹配。同库单体阶段跨模块事务可行(同一 DataSource)。
2. **`customer_transfer` 表结构**:id/transfer_no/handover_user_id/receiver_dean_id/reason(enum)/remark/status(待分配/进行中/已完成)/total_count/assigned_count/create_time/finish_time。
3. **`customer_transfer_item` 子表**:id/transfer_id/customer_id/original_owner/new_owner(初始=接收总监)/final_assignee(分配后=最终销售)/status(待分配/已分配/失败)/fail_reason。
4. **「直属销售总监」怎么定**:读 crm-auth 的 SysDept 树 + 角色配置?→ 需与 crm-auth 现有事实核对,可能加个 `SysUser.superior_user_id` 字段(多角色时用「主直属」)。**本票关键 seam**。
5. **未完成交接锁**:数据层用 `handover_user_id` 上有一条 status ∈ (待分配/进行中) 的记录做互斥;发起前查一次。
6. **跳过与部分成功**:客户分配 tab 按客户逐条校验,任一失败不影响其他客户;返回 Toast 成功/失败数 + 逐条失败原因(部分失败不整体回滚)——**与发起交接的强原子性不同**(发起交接=全批同一事务,分配=按客户独立事务)。
7. **操作日志**:跨模块自动变更操作人显示「系统」+ 注明交接编号(票 07 oplog 字段 `operator_source` 支持 "system+transferNo=XXX")。
## Answer
4 days ago
**Status: resolved**(含 1 个 ⚠ 决策挂号:直属销售总监确定方式)
### A. 流程模型(定稿,两段式)
```
发起交接(离职人员→接收总监) → 客户分配(接收总监→具体销售)
owner 变为总监 owner 变为销售
生成交接单(待分配) 逐客户/批量分配
```
- **只允许离职/调动/区域调整的销售本人发起**本人名下客户的交接(主管/管理员不可代办,原型二.2 明文)。
- **可交接客户** = 该销售为**唯一负责人** 且 未删除 且 非公海;仅协同人的客户不纳入。
- **接收总监**:离职 = 系统自动确定**唯一**直属销售总监;调动/区域调整 = 在本人**启用的**直属销售总监范围内搜索选择。
- **交接单**:全部变更成功才生成(整批原子);生成唯一交接编号 + 状态「待分配」+ 进度 0/N。
### B. 实体(定稿)
#### B1. 交接单 `customer_transfer`
| Java 字段 | 列名 | 类型 | 说明 |
|---|---|---|---|
| id | id | bigint | 主键 |
| transferNo | transfer_no | varchar(32) | 交接编号(唯一,生成规则对称商机编号) |
| fromUserId / fromUserName | from_user_id / from_user_name | — | 离职/调动人员快照 |
| toDirectorId / toDirectorName | to_director_id / to_director_name | — | 接收总监快照 |
| reason | reason | varchar(20) | 交接原因字典:员工离职/岗位调动/区域调整 |
| remark | remark | varchar(500) | 备注(选填) |
| totalCount | total_count | int | 本次交接客户总数 |
| assignedCount | assigned_count | int | 已分配数(进度 = assigned/total) |
| status | status | tinyint | 0待分配 / 1已完成 |
| createBy / createTime / updateBy / updateTime | — | — | BaseEntity |
> 无孤立单大写字母陷阱。
#### B2. 交接明细 `customer_transfer_detail`
| Java 字段 | 列名 | 类型 | 说明 |
|---|---|---|---|
| id | id | bigint | 主键 |
| transferId | transfer_id | bigint | 交接单 id |
| customerId | customer_id | bigint | 客户 id |
| assignStatus | assign_status | tinyint | 0待分配 / 1已分配 |
| assignUserId / assignUserName | assign_user_id / assign_user_name | — | 分配对象快照(待分配时 NULL) |
| assignTime | assign_time | datetime | 分配时间 |
- `UNIQUE(transfer_id, customer_id)` 去重。
### C. 事务语义(定稿,跨模块)
1. **发起交接**(整批原子):
- 遍历本次客户,对每个客户:`owner_user_id → to_director_id`;移出 from_user 团队身份;保留原协同人。
- 对每个客户的**关联商机**(crm-opportunity)`owner_user_id → to_director_id`。
- **关联项目 owner 同步 = seam**(A5 未建;本图只定义 port 契约,实现时项目侧跳过并标注)。
- **任一失败 → 全部回滚**,不生成交接单(原型三.确认交接 §7 明文)。
2. **客户分配**(整批/单个):
- 待分配明细 → 选分配对象(接收总监有权分配范围 + 启用的销售 + 不得为 from_user 本人 + 不得是当前负责人)→ `owner_user_id → assign_user_id`,同步关联商机/项目 owner。
- 单个失败不整体回滚(批量按逐条独立事务),失败项列原因。
3. **防重入**:同一 from_user 存在未完成交接单时,禁止再次发起(原型三.确认交接 §1)。
### D. 契约(定稿)
```
POST /api/customer/transfer/initiate 发起交接(整批原子,返回交接编号)
GET /api/customer/transfer/{id}/preview 发起前客户预览(按原因算可交接客户)
GET /api/customer/transfer/{id} 交接详情
GET /api/customer/transfer/page 交接记录(交接编号/人员/原因/状态过滤)
POST /api/customer/transfer/{id}/assign 客户分配(单个或批量 customerIds + assignUserId)
GET /api/customer/transfer/{id}/assignable 可分配销售列表(接收总监范围 + 启用)
```
### E. ⚠ 挂号:直属销售总监怎么定(需拍板)
原型要求:离职时系统**自动确定唯一直属销售总监**;调动/区域调整时在**启用的直属销售总监范围**选择。核心是「直属销售总监」的确定口径:
**我的推荐**:**不加 SysUser 字段**,用**组织架构现有层级**推导——按 from_user 所在部门向上找「部门负责人」(`dept.leader_user_id` 或等价),若该负责人是「销售总监」角色则命中;无/停用/多条则按原型阻断并提示「联系管理员维护组织关系」。
**理由**:① 组织架构已有部门负责人概念,不引入新字段;② 直属关系是组织属性而非用户属性,加 `superior_user_id` 会带来维护和一致性问题;③ 原型明说「无/停用/多条直属关系 → 阻断」,天然容忍推导失败。
**备选**:`SysUser` 加 `superior_user_id`(直属上级),由后台维护;查询 O(1),但需改 auth 模块 + 维护成本。
> 你选哪个?选推荐则票 08 冻结;选备选则回填 crm-auth 加字段。
### F. 回填 / 下游
- **票 03**:owner 变更(分配=换主)复用 oplog 语义;交割是分配动作的跨模块扩展版。
- **票 07**:oplog 记录 `TRANSFER` 动作 + `customer_transfer` 表变更;跨模块同步操作人显「系统」+ 交接编号。
- **crm-opportunity**:需要商机侧提供「按 customer_id 批量改 owner」的 port(或复用商机分配逻辑)。
- **A5 项目**:项目 owner 同步 seam(port 契约占位)。
- **crm-dict**:`transfer_reason` 分组(员工离职/岗位调动/区域调整)。
### G. 待产品确认(⚠)
| # | 问题 | 影响 |
|---|---|---|
| P0-1 | 交接单「已完成」的判定:assignedCount == totalCount 自动完成,还是总监手动点「完成交接」?原型进度 0/N → **推荐自动完成**(全部分配完即完成) | status 迁移 |