# 线索业务 PRD
**版本**:v1.2(2026 grill-with-docs:接口完整性复核,补 §13 批量操作与统计接口 G1–G5;G6/G7/G8 维持 Out of scope)
**历史**:v1.1(chart 阶段;2026-08-13 续 grill:定时任务顺序、部门集合领取、分配上限、状态CAS、history展示、公海混合展示、转商机事务前提、B/D定稿)
**范围**:crm-lead(线索业务)· crm-rule/线索规则(公海池配置)· crm-preference(列偏好平台能力)
**目的**:把线索域端到端业务流程写成产品可验证的规格;不确定点用 ** ⚠ 待确认** 标出。
---
## 0. 阅读约定
- **⚠ 待确认(编号)**:产品/UX 尚未拍板的点,见文末《待确认事项索引》。
- **术语**沿用 crm-auth 既有:主部门、兼职部门、部门集合、数据范围档位。
- **实体名**用于对齐:线索(Lead)、公海池(LeadPool)、反馈(Feedback)、关注(Follow)、附件(Attachment)、行历史(History)、区划(Region)。
---
## 1. 全景:线索一生
一条线索从**进入系统**到**转化为商机(或作废/失效)**的完整生命周期:
```
┌──────────── 管理员导入/新建 ────────────┐
▼ ▼
[未分发]──分配到池──▶[待领取] [销售新增]
│ │
▼ 领取/分配 ▼
[已领取]────首次反馈────▶[跟进中]
│ │ ▲
│ 释放 / 超时回收 │ │ 再次反馈(重置N)
▼ │ │
[待领取]◀────释放/回收 N ────┘ │
│
转商机(已领取/跟进中) ──────┘
│
▼
[已转商机] ← 终态
失效M计时(创建时起持续跑) ──到期──▶[过期失效]──管理员激活──▶恢复原态
```
**四把标尺**贯穿始终:
1. **状态** (7 态单字段,见 §3)
2. **归属** (哪个池 / 哪个部门 / 哪个销售,见 §4)
3. **两把计时器** (回收 N 天 / 失效 M 天,见 §5)
4. **可见性** (谁能看到、谁能操作,见 §9 三机制权限模型)
---
## 2. 模块架构(后端)
| 模块 | 职责 | 关键实体 |
|---|---|---|
| **crm-lead** | 线索业务:CRUD、领取/反馈/释放/转商机、四视图列表、统计、附件 | `lead` , `lead_history` , `lead_follow` , `lead_attachment` |
| **crm-rule** (线索规则子域) | 公海池配置与规则参数 | `lead_pool` , `lead_pool_province` , `lead_pool_region` , `lead_pool_member` |
| **crm-preference** (新平台模块) | 用户级 UI 列偏好(显隐 + 顺序) | `user_column_preference` |
| **crm-auth** (既有,消费) | 用户/部门/角色/数据范围/**行级授权(新增,ticket 09)** | `sys_user` , `sys_dept` , `sys_role_data_scope` , `sys_row_grant` (设计定稿) |
| **crm-dict** (既有,消费) | 字典:渠道/品牌/需求产品/需求场景 | `dict_group` , `dict_item` |
| **crm-region** (新建,02 定稿) | 行政区划:省/市/区,国标 code | `sys_region` |
| **crm-file** (既有,消费) | 附件文件 | `file` |
| **商机模块** | **不存在** ,crm-lead 通过 outbound port 契约声明依赖 | — |
**依赖关系**:`crm-lead → crm-rule / crm-preference / crm-auth / crm-dict / crm-region / crm-file`;单向,无回边。
---
## 3. 线索状态机(7 态单字段 `leadStatus`)
### 3.1 状态定义
| # | 状态 | 含义 | 有领取人 | 回收计时 N | 失效计时 M |
|---|---|---|:---:|:---:|:---:|
| 1 | **未分发** | 管理员新建/导入且未指定归属池;悬于池外 | 无 | 停 | 跑 |
| 2 | **待领取** | 已归池;池内成员可自领 | 无 | 停 | 跑 |
| 3 | **已领取** | 某销售持有,尚未反馈 | 有 | 跑 | 跑 |
| 4 | **跟进中** | 持有销售已产生至少一次反馈 | 有 | 跑(每次反馈重置) | 跑 |
| 5 | **已转商机** | 已通过转商机操作创建对应商机;**终态** | 保留(历史) | 冻结 | 冻结 |
| 6 | **过期失效** | 失效计时 M 到期;可发生在任何非终态 | 保留(若失效前被持有) | 冻结 | 冻结(除激活重置) |
| 7 | **线索作废** | 反馈=无效自动进入;可恢复 | 进入时清空、分配后重写 | 停 | 跑(继续) |
**反馈情况**(有效 / 无效 / 未反馈)是**独立字段** `feedbackStatus` ,不进状态机;仅供筛选/展示。
### 3.2 合法迁移边
| # | from | 触发 | to | 副作用 |
|---|---|---|---|---|
| 1 | (无) | 管理员新建/导入且未指定池 | 未分发 | 启动 M(起点=创建时刻) |
| 2 | (无) | 销售在"我的线索"新增(自动带出本人池)或管理员指定池 | 待领取 | 启动 M |
| 3 | 待领取 | 销售新增时勾"是否领取" | 已领取 | 领取人=本人;启动 N |
| 4 | 未分发 | 管理员分配到池 | 待领取 | 归池,M 继续 |
| 5 | 未分发 / 待领取 | 管理员分配并指定销售 | 已领取 | 领取人=被指派销售;启动 N |
| 6 | 待领取 | 池内成员点【领取】 | 已领取 | 领取人=本人;启动 N |
| 7 | 已领取 | 首次反馈 | 跟进中 | 写反馈;N 重置(起点=反馈时刻) |
| 8 | 跟进中 | 再次反馈 | 跟进中(自环) | 写反馈;N 重置 |
| 9 | 已领取 / 跟进中 | 转商机 | 已转商机 | 调 OpportunityCreationPort;写 CONVERT 历史;冻结 N、M |
| 10 | 已领取 / 跟进中 | 手动【释放】 | 待领取 | 清领取人;N 停 |
| 11 | 已领取 / 跟进中 | 超时回收(N 到期无跟进) | 待领取 | 清领取人;N 停 |
| 12 | 未分发 / 待领取 / 已领取 / 跟进中 / 线索作废 | 失效计时 M 到期 | 过期失效 | 若被持有则保留领取人;只读(作废态失效后 status_before_expire=线索作废,激活恢复作废) |
| 13 | 过期失效 | 管理员【激活】(单条/批量) | 恢复失效前状态与原领取人 | **N 和 M 均重置** (起点=激活时刻,`recycle_deadline = now + pool.N`,`expire_deadline = now + pool.M`)。若失效前态=线索作废则恢复作废(等同无操作,见 §3.4) |
| 14 | 已领取 / 跟进中 | 提交反馈且反馈情况=无效 | 线索作废 | **清空 owner_user_id** ;M 继续跑;写 VOID 历史 |
| 15 | 线索作废 | 管理员分配给销售 | 线索作废(自环) | 写入新 owner_user_id;状态不变;写 ASSIGN 历史 |
| 16 | 线索作废 | 被分配销售提交反馈且反馈情况=有效 | 已领取 | owner=当前销售;启动 N;写 FEEDBACK 历史 |
### 3.3 状态 × 操作 权威矩阵
| 操作 | 未分发 | 待领取 | 已领取 | 跟进中 | 已转商机 | 过期失效 | 线索作废 |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 详情 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 编辑 | ✓(管理) | ✓(管理) | ✓(持有人) | ✓(持有人) | — | — | ✓ |
| 领取 | — | ✓(池成员) | — | — | — | — | — |
| 分配 | ✓(管理) | ✓(管理) | — | — | — | — | ✓(管理) |
| 反馈 | — | — | ✓(持有人) | ✓(持有人) | — | — | ✓(持有人) |
| 转商机 | — | — | ✓(持有人) | ✓(持有人) | — | — | — |
| 释放 | — | — | ✓(持有人) | ✓(持有人) | — | — | — |
| 关注/取关 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 删除 | ✓(管理/创建人) | ✓(管理/创建人) | ✓(管理/创建人·强确认) | ✓(管理/创建人·强确认) | ** ✕ 禁止**(终态) | ✓(管理/创建人·强确认) | ✓(管理/创建人·强确认) |
| 激活 | — | — | — | — | — | ✓(管理) | — |
「管理」= 具有该线索所属数据范围的管理权限;具体谁是"管理"随 §9 权限 PRD 定。
> **删除权限全局规则(2026-08-13 定稿)**:`删除 = (状态 ≠ 已转商机) AND (管理员 OR 创建人·强确认)`。**已转商机是唯一删除禁区**(终态,下游商机 `source_lead_id` 依赖,删会悬空);过期失效/线索作废无下游依赖,仍可删。推翻了早期“持有人可删过期失效”的写法,改为“创建人可删”。
### 3.4 线索作废生命周期(2026-08-13 产品定稿,原 ⚠(A))
```
已领取/跟进中 ─[提交反馈,反馈情况=无效]→ 线索作废(清空 owner)
│ ▲
管理员分配给新销售(仍保持作废)
新销售提交反馈,反馈情况=有效 → 已领取
└──┘
```
- **触发**:销售提交反馈且反馈情况=无效时**自动**进入,无需手动点按钮。
- **入口态**:已领取 / 跟进中(能提交反馈的状态)。
- **可恢复**:管理员可把作废线索分配给其他销售(分配后仍保持作废态);被分配销售提交反馈=有效时自动恢复到“已领取”。
- **归属**:进入作废时清空 `owner_user_id` ;分配时写入新 owner。
- **计时器**:失效计时 M 在作废态下**继续跑**(不冻结)。
- **作废态可操作**:详情 / 编辑 / 反馈(持有人)/ 分配(管理)/ 关注 / 删除(管理员 OR 创建人·强确认)。
- **激活与作废的关系(2026-08-13 拍板)**:【激活】是**过期失效专属**操作;作废线索**不提供激活**,复活只有“分配 → 新销售反馈=有效”一条路。
- 由于 M 在作废态继续跑,作废线索可能 M 到期进入【过期失效】,此时“失效前态=线索作废”。
- 对这种线索执行【激活】会恢复到“线索作废”(owner 仍为空)——**等于无操作**,实际不会/不应对作废线索点激活。复活仍走分配。
- 工程含义:状态机里“过期失效 → 激活”只恢复失效前态;不需为作废特制新边。
---
## 4. 归属模型
### 4.1 三层归属
一条线索的归属由**三层**表达,各有用途:
| 层 | 字段 | 由什么决定 | 变化时机 |
|---|---|---|---|
| **池归属** | `pool_id` | 建线索时指定(或后续分配) | 分配变更 |
| **部门归属** | `dept_id` (冗余 = 池的 dept_id) | 池所属部门;**一部门一池** → 线索的 dept_id 恒等于池的 dept_id | 池不变则永不变 |
| **销售归属** | `owner_user_id` | 领取/分配到销售 | 领取/释放/回收/分配 |
### 4.2 快照策略
以下字段在 lead 主表以**快照**形式冗余,避免关联查询:
| 字段 | 快照源 | 刷新时机 |
|---|---|---|
| `pool_name_snapshot` | `lead_pool.pool_name` | 池改名时批量刷 |
| `dept_id` | 池的 dept_id | 一部门一池 → 永不变 |
| `team_dept_id` | 池的 team_dept_id | 同上 |
| `owner_name_snapshot` | 领取人姓名 | 领取/分配时快照,释放时清空 |
| `owner_dept_id_snapshot` | 领取时销售所属部门 | 领取/分配时快照(用于审计"这条线索历史属于哪个部门") |
| `feedback_*` (5 列) | 最新一条反馈 | 每次反馈时刷 |
**⚠ 待确认(B)**:销售换部门后,`owner_dept_id_snapshot` 是否需要刷新?当前定为**不刷**(快照即历史)。
### 4.1.1 公海池换部门时的 dept_id 联动
管理员可修改公海池的归属部门(如从 A 部门改挂到 B 部门)。此时线索 `dept_id` 的联动规则:
| 线索状态 | dept_id 是否跟池走 | 说明 |
|---|:---:|---|
| 未分发 / 待领取 | ✓ 跟池走 | 批量 `UPDATE lead SET dept_id = new_dept_id WHERE pool_id=? AND status IN ('未分发','待领取')` ,新部门成员可查到、可领取 |
| 已领取 / 跟进中 | ✗ 不动 | `dept_id` 保持原值(=旧部门),已归属销售的私海线索不受池换部门影响 |
| 已转商机 / 过期失效 / 线索作废 | ✗ 不动 | 终态/失效线索不参与 |
**前置校验**:目标部门必须当前**无池**(`UNIQUE(dept_id)` 约束);源部门换出后无池不拦截。
**改写 [I]**:`dept_id` 不再是"恒定永不变动",而是"池归属部门变更时,待领取/未分发线索 dept_id 跟池走;已领取/跟进中线索 dept_id 不动"。
### 4.3 公海池(`lead_pool`,crm-rule 定义)
一个公海池 = 一个**部门**的线索容器 + 一组**规则参数**:
| 字段组 | 内容 |
|---|---|
| **归属** | `dept_id` (UNIQUE,一部门一池)、`team_dept_id`(冗余,团队 = 部门上溯节点) |
| **负责市场** | 多省(`lead_pool_province`)+ 多市/区(`lead_pool_region`);用国标 `region_code` 前缀 LIKE 支持祖先查询 |
| **人员** | `lead_pool_member(pool_id, user_id, role_type)` role=OWNER(1)/COLLABORATOR(多);部门成员 = 部门全体动态推导,不落库 |
| **规则参数** | `recycle_days` (N,默认 7)、`expire_days`(M,默认 180)、`hold_limit`(持有上限,默认 200)、`daily_claim_limit`(每日领取上限,默认 10,0=无限)、`claim_rule`(1=成员可领+管理员分配 / 2=仅管理员分配) |
**池名**全局 UNIQUE;**删除**软删且必须"池下无非终态线索"。
**换部门**:管理员可修改 `dept_id` ,前置校验目标部门当前无池(`UNIQUE(dept_id)`)。换部门后,待领取/未分发线索 `dept_id` 批量跟池走,已领取/跟进中线索 `dept_id` 不动(见 §4.1.1)。
---
## 5. 双计时器
**两把独立计时器,各管各的**(对齐产品最新口径):
### 5.1 回收计时 N(`recycle_deadline`)
- **计什么**:销售领取到线索后,多久没跟进就退回公海。
- **起点**:领取时刻。
- **重置**:每次新增反馈时重置为 `now + N` 。
- **停止**:释放 / 回收 / 转商机 / 失效。
- **到期动作**:定时任务扫 `WHERE recycle_deadline < now AND status IN ('已领取','跟进中')` → 状态回到"待领取"、清领取人、写 RECYCLE 历史。
- **存储**:`lead.recycle_deadline datetime`,加索引。
### 5.2 失效计时 M(`expire_deadline`)
- **计什么**:一条线索从入库起多久后自动失效。
- **起点**:**创建时刻**,不随领取/释放变化。
- **重置**:**仅**管理员激活时(`now + M`)。
- **停止**:转商机 / 已失效。
- **到期动作**:定时任务扫 `WHERE expire_deadline < now AND status IN ('未分发','待领取','已领取','跟进中','线索作废')` → 状态置"过期失效"(保留领取人若有)、写 EXPIRE 历史。**含线索作废**:作废态 M 继续跑(§3.4),到期同样置失效(status_before_expire=线索作废)。
- **存储**:`lead.expire_deadline datetime`,加索引。
### 5.3 池规则参数变更时的批量刷新
管理员改了池的 N 或 M:
- N 改动 → 分状态批量刷新:
- 已领取:`UPDATE lead SET recycle_deadline = claim_time + new_N WHERE pool_id=? AND status='已领取'`
- 跟进中:`UPDATE lead SET recycle_deadline = last_feedback_time + new_N WHERE pool_id=? AND status='跟进中'`
- M 改动 → 批量 `UPDATE lead SET expire_deadline = create_time + new_M WHERE pool_id=? AND status != '已转商机'`
**当日不批量清算已超时的**——只改截止时刻,实际状态转移交给次日定时任务自然执行(对齐原型 A7-3-1 口径)。
---
## 6. 关键业务流程
### 6.0 全局并发原则:状态 CAS 乐观锁(2026-08-13 grill #8 定稿,见 ADR-0021)
**所有状态迁移的 UPDATE 一律带 `AND status IN (合法起始态)` 作 CAS;行数=0 即判定「已被他人操作」失败。**
- 适用:领取(#6)/ 分配(#4/#5/#15)/ 激活(#13)/ 释放(#10)/ 反馈(#7/#8/#16)/ 转商机(#9)全部迁移。
- “部分成功”的“失败”有确切定义 = CAS 行数 0(目标已被并发改态,或被定时任务回收/失效)。批量领取/分配/激活均非原子,逐条 CAS,成功 N 条、失败 M 条弹提示。
- 例:两销售同时领取同一条待领取线索 → `UPDATE lead SET owner_user_id=? WHERE id=? AND status='待领取'` ,后者行数=0,不会静默抢走。
- 定时任务(回收/失效)的 `WHERE ... AND status IN (...)` 同属此原则,与人工操作天然互斥。
### 6.1 新增线索(销售侧 · a2-1-2-2)
**入口**:【我的线索】列表 → 【新增线索】按钮。
**表单字段**(必填标 `*` ):
| 分组 | 字段 | 类型 | 校验 |
|---|---|---|---|
| 基础 | 线索名称 | 文本 | 无 |
| | 联系电话 | 文本 | 手机/座机通用正则(单字段) |
| | 微信 | 文本 | 微信号正则;无效提示"无效微信号" |
| | 电子邮箱 | 文本 | 通用邮箱正则;无效提示"无效邮箱号" |
| | 省份 * | 下拉 | 联动城市 |
| | 城市 * | 下拉 | 依赖省份;未选省份不可选城市 |
| | 详细地址 | 文本 | 无 |
| 业务 | 渠道 * | 字典 | dict group=`lead.channel` |
| | 品牌 * | 字典 | dict group=`lead.brand` |
| | 需求产品 * | 字典 | dict group=`lead.product` |
| | 需求场景 | 字典 | dict group=`lead.scene` |
| 核心 | 咨询内容 * | 多行文本 | 无 |
| 只读回填 | 线索状态 | 展示"待领取" | 不可编辑 |
| 只读回填 | 线索公海池 | 自动带出本人所属池 | 不可编辑 |
| | 销售团队 / 销售部门 / 销售人员 / 新增时间 | 自动回填 | 不可编辑 |
| 标记 | 是否加急 | 单选是/否,默认否 | — |
| | 是否领取 | 复选,勾则新增即领 | — |
| | 是否关注 | 复选,勾则加我的关注 | — |
**底部**:连续新增(复选)· 新增线索(主按钮)· 关闭(二次确认放弃)。
**前置校验(2026-08-13 grill #6 )**:
- **自动带池 = 本人主部门的池**(不给选、不可编辑);即使兼职部门也有池,新增仍只落主部门池。跨部门归属走管理员【分配】。
- **主部门无池 → 报错拦截**(“本部门尚未配置公海池,请联系管理员”),不允许销售建挂空池线索;“未分发/空池”是管理员导入专属中间态。
**副作用**:
- 落 `lead` 行 → 状态 = 待领取(勾"是否领取"则一步转已领取,走边 #3 )
- 写 `lead_history` CREATE 记录
- 勾"是否关注" → 落 `lead_follow`
- 触发失效计时 M(起点=`create_time`);若同步领取,触发回收计时 N
### 6.2 领取线索(池成员 · a2-1-1 系列)
**入口**:【线索公海】列表 → 单条【领取】 / 批量勾选后【批量领取】。
**前置**(应用层校验):
1. 当前用户在池的可领取成员集合内(`claim_rule=1` 时=部门全员;`claim_rule=2` 时**销售不可自领**,按钮隐藏)。**“部门全员”按部门集合算**(2026-08-13 grill #5 ):销售可领取其所属**任一部门(主+兼职)**公海池的线索,即 `pool.dept_id IN 用户部门集合` 。与 crm-auth 既有 ABAC `DEPT` 档口径一致(ADR-0005,部门集合=主+兼职并集),“能领的=能看的”自洽。
2. 未超"每日领取上限"`daily_claim_limit`(0=无限);**"每日" = 自然日**(00:00-23:59,`DATE(op_time) = CURRENT_DATE`)
3. 未超"个人持有上限"`hold_limit`;**计数口径 = 仅已领取 + 跟进中**(已转商机、过期失效、线索作废不占名额)
4. 目标线索状态 = 待领取
**副作用**:设 `owner_user_id` ;快照 owner 姓名/部门;状态转 已领取(边 #6 );启动 N;写 CLAIM 历史。
### 6.3 分配线索(管理侧 · A7 相关,部分在 A2 管理视图)
**入口**:管理员在【线索管理】/【线索公海】的管理视图选中线索 → 【分配】。
**两种分配深度**:
- **仅分配到池**(未分发 → 待领取,边 #4 )
- **分配到销售**(未分发/待领取 → 已领取,边 #5 )
**权限**:仅管理员 + 池 OWNER + 池 COLLABORATOR 可执行(对齐 04-3 语义)。
**上限校验(2026-08-13 grill #7 )**:分配到销售时,`hold_limit`(个人持有上限)与 `daily_claim_limit` (每日领取上限)**都校验**,与自领同一套约束(分配也受限)。
- **额度归属**:消耗的是**被指派销售**的持有数/当日领取计数(不是管理员),按**目标线索所属池**的上限值算。
- **批量分配部分成功**:超限不拖垮整批——成功 N 条、失败 M 条(超限/已被他人操作)弹提示,非原子,与批量激活/批量领取口径统一。
- 仅分配到池(未分发→待领取,无 owner)不占任何上限。
### 6.4 反馈(持有人 · a2-1-2 系列)
**入口**:详情页 / 列表操作【反馈】按钮。
**表单**:反馈情况(有效/无效/未反馈)· 反馈内容 · 需求产品(可切换)· 附件(走 crm-file,存 `lead_feedback.attachment_ids` )。
**草稿/提交双态**(改写 [Q2=c],拆出独立 `lead_feedback` 表):
- **【保存草稿】**:写/更新 `lead_feedback` 行 `record_status='DRAFT'` ,**不校验必填**,不改状态机,不写 `lead_history` ,不刷主表快照。草稿按 `(user_id, lead_id)` 唯一,重复保存为覆盖写。
- **【保存】(正式提交)**:`lead_feedback` 行 `record_status='SUBMITTED'` ,同时:
- 主表更新 5 列反馈快照(`feedback_status/content/time/by/product_code`)
- 状态:已领取 → 跟进中(边 #7 );跟进中 → 跟进中(边 #8 )
- **回收计时 N 重置**(起点=`now`)
- 写 `lead_history` FEEDBACK 记录(detail 引用 `lead_feedback.id` ,不冗余内容)
- 清除同 `(user_id, lead_id)` 的草稿行(转为 SUBMITTED)
**`lead_feedback` 表结构**:
```
lead_feedback
├─ id, lead_id, user_id
├─ feedback_status, content, product_code
├─ attachment_ids json -- crm-file 文件 id 列表
├─ record_status tinyint -- 1=DRAFT, 2=SUBMITTED
├─ created_at, submitted_at
└─ INDEX(lead_id, user_id, record_status)
```
### 6.5 转商机(持有人 · a2-1-2 转商机弹窗)
**入口**:详情页 / 列表【转商机】按钮。
**前置**:`status IN (已领取, 跟进中)`;当前用户=持有人;反馈情况**不参与判定**(对齐产品最新口径,⚠ 已推翻原型"仅有效/跟进中/已联系"文案,UX 修订清单见 §11)。
**表单**:商机名称* · 行业*(字典 `opportunity.industry` ,⚠ 待商机模块创建)· 甲方 · 意向客户* · 地区*(region_code)· 备注。
**副作用(同一本地事务)**:
1. 调 `OpportunityCreationPort#createOpportunity(cmd)` → 拿商机 id
2. `UPDATE lead SET status='已转商机' WHERE id=? AND status IN ('已领取','跟进中')` ;行数=0 抛并发异常
3. 写 `lead_history` CONVERT 记录,detail 含 `opportunityId + opportunityName`
4. 冻结 N/M(靠定时任务 `WHERE status != '已转商机'` 天然实现,无需 UPDATE 截止字段)
**幂等**:状态终态保护(crm-lead 侧) + `opportunity` 表 `UNIQUE(source_lead_id)` (商机侧契约要求)。
**失败**:本地事务性回滚,线索状态不变。
**⚠ 事务语义前提约束(2026-08-13 grill #10 ,见 ADR-0020)**:上述“同一本地事务”成立的**前提**是 `OpportunityCreationPort` 的**首个实现与 crm-lead 同库、共享事务**(商机模块尚未建,近期大概率同库)。此前提写入端口契约(ticket 08)。若未来商机模块**独立库/独立部署**,本节事务语义**必须重新设计**:改为“先本地 CAS 改态 → 再调端口建商机”的最终一致 + 补偿(靠 `UNIQUE(source_lead_id)` 幂等 + 对账收敛)。现阶段不过度设计。
### 6.6 释放(持有人 · a2-1-2)
**前置**:`status IN (已领取, 跟进中)`;当前用户=持有人。
**副作用**:清 `owner_user_id` 、`owner_name_snapshot`、`owner_dept_id_snapshot`、`claim_time`、`recycle_deadline`;状态转"待领取"(边 #10 );N 停;写 RELEASE 历史。**反馈快照 5 列不清空**(保留上一位持有人的反馈,供下一位领取人参考)。线索回到池内待领取,原持有人失去所有非查看权限。
### 6.7 定时任务:超时回收 & 失效
**回收 Job**(每日凌晨 / 或按配置节奏):
```sql
SELECT id, pool_id, owner_user_id FROM lead
WHERE recycle_deadline < now ( ) AND status IN ( ' 已领取 ' , ' 跟进中 ' ) ;
```
逐行:清领取人,状态→待领取,写 RECYCLE 历史。
**失效 Job**(同上):
```sql
SELECT id, status FROM lead
WHERE expire_deadline < now ( ) AND status IN ( ' 未分发 ' , ' 待领取 ' , ' 已领取 ' , ' 跟进中 ' , ' 线索作废 ' ) ;
```
逐行:状态→过期失效(若在私海则保留 owner_user_id 作历史记录),写 EXPIRE 历史。
**执行顺序(关键,2026-08-13 grill #4 定稿,见 ADR-0019)**:**失效 Job 先跑,回收 Job 后跑。** 当一条线索两个 deadline 同时过期时,失效优先:
- 失效把状态改为「过期失效」(保留 owner)后,回收 Job 的 `status IN ('已领取','跟进中')` 天然扫不到它,两者不再竞争。
- 语义理由:失效是更强的终止语义,“谁持有”这个历史应被冻结保留;且失效可被管理员【激活】连 owner 一起恢复,而回收退公海会不可逆地丢 owner。先失效给管理员留一条“激活找回原持有人”的后路。
**⚠ 待确认(D)**:两个 Job 的执行节奏(每日凌晨?每小时?按公海池独立?)。当前定为**统一每日 02:00 全库扫**(同一次扫描内先失效后回收)。
### 6.8 激活失效线索(管理员)
**入口**:过期失效线索详情 → 【激活】(单条);线索管理列表 → 勾选多条 → 【批量激活】。
**副作用**:状态恢复为失效前状态、owner 保留;`expire_deadline = now + pool.expire_days`(M 重置);**`recycle_deadline = now + pool.recycle_days`(N 同步重置)**;写 ACTIVATE 历史。
**批量激活**:逐条执行单条激活逻辑,**部分成功**——成功 N 条、失败 M 条(已被他人操作),弹结果提示。非原子。
### 6.9 编辑线索(管理/持有人)
**入口**:列表操作【编辑】按钮,打开线索详情编辑页(含【线索详情】【历史记录】双 Tab)。
**可编辑字段**(= 新增线索表单的业务字段):
| 分组 | 可编辑字段 |
|---|---|
| 基础 | 线索名称、联系电话、微信、电子邮箱、省份、城市、详细地址 |
| 业务来源 | 渠道、品牌、需求产品、需求场景 |
| 核心 | 咨询内容、是否加急 |
**不可编辑字段**:状态、所属公海池、领取人、领取时刻、所有时效字段(`recycle_deadline`/`expire_deadline`)、所有快照字段(`pool_name_snapshot`/`owner_*`/`feedback_*`)——由状态机和业务流程管,不走编辑入口。
**权限**:
- 未分发 / 待领取 → **管理**可编辑
- 已领取 / 跟进中 → **持有人**可编辑
- 已转商机 / 过期失效 → **不可编辑** (终态锁 / 只读)
- 线索作废 → **可编辑** (业务字段,与已领取/跟进中同)
**副作用**:
- 写 `lead_history` EDIT 记录,detail 含变更字段清单(旧值→新值)
- **不改状态**,不影响 N/M 计时器
### 6.10 操作日志 `lead_history`(2026-08-13 grill #3 定稿)
**写入口径 = 全量记录(11 种类型),展示口径 = 仅 5 种(selected (a))。**
`lead_history.type` 枚举(全量落库):
| type | 触发 | detail 关键内容 | 页面展示 |
|---|---|---|:---:|
| CREATE | 新建/导入(§6.1) | — | ✕ |
| CLAIM | 领取(边 #6 ) | — | ✅ 领取了该线索 |
| ASSIGN | 分配(边 #4/#5/#15 ) | 新 owner | ✕ |
| FEEDBACK | 反馈(边 #7/#8/#16 ) | 引用 `lead_feedback.id` | ✅ 添加了反馈,状态修改为【状态】,添加内容【反馈原文内容】 |
| CONVERT | 转商机(边 #9 ) | opportunityId + opportunityName | ✅ 将线索转化为项目【项目名称】,生成对应商机 |
| RELEASE | 释放(边 #10 ) | — | ✅ 将该线索释放回线索公海池 |
| RECYCLE | 超时回收(边 #11 ) | 原持有人 | ✕ |
| EXPIRE | 失效(边 #12 ) | — | ✕ |
| ACTIVATE | 激活(边 #13 ) | — | ✕ |
| VOID | 作废(边 #14 ) | **原持有人 `owner_user_id`** (作废清 owner 前留痕,供审计) | ✕ |
| EDIT | 编辑(§6.9) | 变更字段清单(旧值→新值) | ✕ |
- **全量写**:11 种动作全部落 `lead_history` ,作为管理员审计与纠纷排查的唯一来源。
- **页面展示**:详情页「历史记录」Tab 只 `WHERE type IN (CLAIM, RELEASE, FEEDBACK, CONVERT, EDIT)` 过滤展示 5 种。
- **作废审计**:VOID 记录 detail 保留原持有人;`lead_feedback.user_id`(谁反馈无效)与 `lead_history` 均不受清 owner 影响,无主只是当前归属,历史全程可追溯。
---
## 7. 四视图(前端列表页)
| 视图 | 路径 | 数据集 | 关键操作 |
|---|---|---|---|
| **线索公海** (a2-1-1) | /lead/pool | 池内**非终态、非失效**线索(待领取 + 已领取/跟进中 的只读观察态,防撞单),部门集合天花板 | 领取、关注 |
| **我的线索** (a2-1-2) | /lead/mine | `owner_user_id = 当前用户` (含已转商机、过期失效) | 反馈、转商机、释放、编辑、删除 |
| **我的关注** (a2-1-3) | /lead/follow | 我在 `lead_follow` 里关注的线索 | 取消关注、跳转详情 |
| **线索管理** (a2-1-4) | /lead/manage | 数据可见范围内**全部**线索 | 分配、导入/导出、批量释放、删除、激活 |
**列偏好**:每视图**独立一套**字段显隐 + 顺序(scope_key 各不同:`lead.public_pool` / `lead.my_lead` / `lead.my_follow` / `lead.manage` ),互不同步(对齐 07 定稿,⚠ 已推翻原型"共用一套"文案,UX 修订清单见 §11)。
**✅ 已定稿(E)(2026-08-13 grill #9 )**:线索公海视图**混合展示**——含「待领取」+「已被别人领取」(已领取/跟进中)的只读观察态,用于防撞单(领前确认同事是否已在跟)。**展示范围作为可配置项,不硬编码**(后续可能收窄为仅待领取,或按池/角色细分):后端以一个“公海可见状态集”参数(默认 `{待领取, 已领取, 跟进中}` )驱动过滤,避免改规则时改代码。领取按钮仅对 `status='待领取'` 行可点,已被领走的行只读。
---
## 8. crm-preference 契约(列偏好,07 定稿)
**表**:`user_column_preference(id, user_id, scope_key varchar(64), visible_keys json, column_order json, updated_at)`,`UNIQUE(user_id, scope_key)`。
**接口**(仅 2 方法):
```java
Optional< ColumnPreference > get(Long userId, String scopeKey);
void save(Long userId, String scopeKey, ColumnPreference preference);
record ColumnPreference(List< String > visibleKeys, List< String > columnOrder) {}
```
**scope_key**:由业务方(crm-lead)约定的**稳定字符串**(不是路由、不是主键)。线索域 4 个:
- `lead.public_pool` · `lead.my_lead` · `lead.my_follow` · `lead.manage`
**字段池归属**:业务方(crm-lead)拥有字段元数据、校验、未知 key 兜底;preference 只存"勾了哪些 key + 什么顺序",无感知语义。
**保存时机**:**仅**点【保存】按钮才落库;拖拽临时顺序仅前端本地保留,关页面/刷新即丢。
**明确不做**:跨用户默认模板、管理员强制列、多租户 scope 隔离、偏好版本历史、字段池注册表、scopeKey 注册表校验。
---
## 9. 数据可见性与权限
**§9 已定稿(2026-08-13,详见 `权限与数据可见性-PRD.md` + `issues/05` + `issues/09` )**:
三机制模型 **RBAC + ABAC + ReBAC** (升级动因:团队成员/线索池协作人这类"行级授权"必须能越过部门天花板,纯 AND 不够表达):
1. **RBAC 功能权限** (既有):跟角色走;权限点控制按钮/API 开关。
2. **ABAC 数据可见范围** (既有,四档天花板):页面业务语义(owner=我/dept=本部门)+ SELF / DEPT / DEPT_AND_CHILDREN / ALL,多角色取最宽。
3. **ReBAC 行级授权(通行证)** (新增,见 ticket 09):`sys_row_grant(user_id, resource_type, resource_id, created_by, created_at)` 通用表;拦截器 `OR EXISTS(...)` 注入;**跟用户不跟角色**;**只覆盖部门天花板,不覆盖页面业务语义**。
**组合公式**:
```
能否看到某数据 = 页面业务条件(ABAC语义) AND (落在数据可见范围(ABAC天花板) OR 已被行级授权(ReBAC))
```
**5 项产品拍板(PRD-1/2/3/D/E)已全部定稿**,结论如下:
- **PRD-1 触发动作清单**:团队成员(商机/客户/项目,per-row)+ 公海池负责人/协作人(线索,池维度 `resource_type=pool` )。
- **PRD-2 权限边界**:被授权者**只读**;写权由权限点+service(owner/状态机)守,不分档。
- **PRD-3 撤销立即失效**:硬删(`DELETE FROM sys_row_grant`),不做软删。
- **PRD-D 线索不开按单条直接授权([D])**:线索可见=owner=我 OR 池级授权;无 `resource_type=lead` ;分配=改 owner_id。
- **PRD-E 池配置不发通行证([E])**:池配置可见/可改=功能权限×数据范围;负责人/协作人身份不授予池配置写权。
- **[F] 关注不走行级授权**:“我的关注”=`id IN 关注表` AND 部门天花板,每请求实时算。
---
## 10. 依赖与集成
| 目标 | 依赖模块 | 依赖形态 |
|---|---|---|
| 用户/部门/角色 | crm-auth | 消费既有 API |
| 数据范围拦截 | crm-auth | `@DataScope` 注解 + 拦截器 |
| **行级授权** | crm-auth(**已立项 ticket 09**) | `sys_row_grant` 通用表 + 拦截器 OR EXISTS 注入(设计定稿,待实现) |
| 字典 | crm-dict | 4 个 group:`lead.channel/brand/product/scene` |
| 行政区划 | crm-region(新建,02 定稿) | `sys_region.code` 国标前缀 LIKE 查询 |
| 附件 | crm-file | 关联表 `lead_attachment(lead_id, file_id)` |
| 列偏好 | crm-preference(新建,07 定稿) | 2-方法接口 |
| **商机模块** | **不存在** | crm-lead 定 outbound port `OpportunityCreationPort` ,实现待商机模块落地 |
---
## 11. 待确认事项索引
### 11.1 产品拍板类(阻塞 spec 收尾)
| 编号 | 事项 | 阻塞对象 | 优先级 |
|---|---|:---:|:---:|
| ~~⚠ (A)~~ | ~~线索作废~~ ** ✅ 已定稿(2026-08-13)**:反馈=无效自动进入,入口=已领取/跟进中,清空 owner,可恢复(分配+反馈=有效→已领取)。见 §3.4 | — | — |
| ~~⚠ (B)~~ | ~~销售换部门后 `owner_dept_id_snapshot` 是否刷新?~~ ** ✅ 定稿(2026-08-13 grill #11 )**:**不刷**,快照即历史(审计用)。 | — | — |
| ~~⚠ (C)~~ | ~~反馈是否可上传附件?~~ ** ✅ 已解决**:反馈可上传附件,存 `lead_feedback.attachment_ids` (本轮 grill #3 ) | — | — |
| ~~⚠ (D)~~ | ~~回收/失效 Job 执行节奏~~ ** ✅ 定稿(2026-08-13 grill #11 )**:**每日 02:00 全库扫**(失效先、回收后,见 §6.7)。 | — | — |
| ~~⚠ (E)~~ | ~~线索公海视图是否显示"已被别人领取"的线索~~ ** ✅ 已定稿(2026-08-13)**:混合展示(防撞单),展示范围可配置不硬编码。见 §7 | — | — |
| ** ⚠ PRD-1** | 行级授权**触发动作清单** | ~~05、§9~~ | ** ✅ 定稿**:团队成员+池负责人/协作人 |
| ** ⚠ PRD-2** | 被授权者**权限边界** | ~~05、§9~~ | ** ✅ 定稿**:只读,写权归 service |
| ** ⚠ PRD-3** | 移除授权后**即时失效** | ~~05、§9~~ | ** ✅ 定稿**:硬删,立即失效 |
| ** ⚠ PRD-D** | 线索是否**跨部门逐条指派** | ~~05、§9~~ | ** ✅ 定稿**:不开 lead 直接通道,分配=改 owner |
| ** ⚠ PRD-E** | 池负责人/协作人的**授权范围** | ~~05、§9~~ | ** ✅ 定稿**:仅池内线索可见,不兼池配置写权 |
### 11.2 UX 修订类(不阻塞后端,但上线前必改,详见 `待确认事项清单.md`)
| 编号 | 事项 | 处数 |
|---|---|:---:|
| ** ⚠ UX-1** | 列表字段"共用一套/同步生效" → "每菜单独立" | 7 处 |
| ** ⚠ UX-2** | "拖拽实时保存" → "点保存才落库" | 2 处 |
| ** ⚠ UX-3** | 公海池"江苏 1/2/3" → 合并为 1 池 | 数据行 |
| ** ⚠ UX-4** | 转商机触发条件"跟进中/已联系/有效" → "已领取/跟进中" | 3 处 4 行 |
---
## 12. 附:字段清单速查
见各 ticket Answer:
- `lead` 主表 → `issues/06-线索实体与字段模型.md`
- `lead_feedback` (独立反馈实体,DRAFT/SUBMITTED) → 本 PRD §6.4(本轮 grill #3 改写 [Q2=c])
- `lead_pool` + 3 关联表 → `issues/04-公海池实体与归属建模.md`
- `sys_region` → `issues/02-行政区划数据源调研.md`
- `user_column_preference` → `issues/07-preference列偏好scope模型与契约.md`
- `OpportunityCreationPort` → `issues/08-转商机outbound-port契约.md`
- `sys_row_grant` (行级授权底座) → `issues/09-行级授权sys_row_grant能力.md`
- 权限三机制完整推导 → `权限与数据可见性-PRD.md` + `issues/05-线索域数据隔离落地.md`
- 模块领域语言 → `crm-lead/CONTEXT.md` 、`crm-rule/CONTEXT.md`、`crm-preference/CONTEXT.md`
### 12.1 grill 决策清单(2026-08-12)
| # | 决策 | 影响章节 |
|---|---|---|
| 1 | 池换部门允许;待领取/未分发 `dept_id` 跟池走,已领取/跟进中不动;目标部门须无池 | §4.1.1、§4.3,改写 [I] |
| 2 | 线索合并不做,Out of scope | map.md |
| 3 | 拆出 `lead_feedback` 独立表(DRAFT/SUBMITTED),改写 [Q2=c] | §6.4 |
| 4 | 过期失效线索可删(管理/持有人·强确认) | §3.3 矩阵 |
| 5 | 释放/回收时反馈快照不清空 | §6.6 |
| 6 | 激活时 N 和 M 均重置 | §3.2 边 #13 、§6.8 |
| 7 | `hold_limit` 只算已领取 + 跟进中 | §6.2 |
| 8 | 批量激活,部分成功 | §6.8 |
| 9 | `daily_claim_limit` "每日" = 自然日 | §6.2 |
| 10 | 导入线索不做,留 fog | map.md |
| 11 | 销售可删自己过期失效线索(持有人·强确认) | §3.3 矩阵 |
| 12 | 编辑范围 = 业务字段可改,状态/池/归属/时效/快照不可改 | §6.9 |
### 12.2 grill 决策清单(2026-08-13,本轮收口)
| # | 决策 | 影响章节 |
|---|---|---|
| 13 | 线索作废:反馈=无效自动触发,入口=已领取/跟进中,清空 owner,可恢复(分配+反馈=有效→已领取) | §3.1/3.2/3.3/3.4 |
| 14 | 删除权限全局修正 = (状态≠已转商机) AND (管理员 OR 创建人·强确认);已转商机为删除禁区 | §3.3 |
| 15 | 权限三机制 RBAC+ABAC+ReBAC 定稿;ReBAC 跟用户不跟角色、只覆盖天花板 | §9 |
| 16 | PRD-1~E 全部拍板;`sys_row_grant` 立项为 ticket 09 | §9、§11.1 |
| 17 | 激活是过期失效专属;作废无激活(只有分配复活);作废→失效→激活=恢复作废(等同无操作)(prototype-permission / prototype-statemachine 验出) | §3.2#13、§3.4、§6.8 |
| 18 | 删除“创建人可删”对普通销售在“自建且自持有(owner=我)”场景由“我的线索”行使;流走后归属变,删除权由新持有方/管理员承接(自洽,无需改) | §3.3 |
### 12.3 grill 决策清单(2026-08-13 本轮续 grill)
| # | 决策 | 影响章节 |
|---|---|---|
| G1 | 作废态反馈/编辑的“持有人”不矛盾(视角问题):“我的线索”里的作废线索 owner 必=我;“线索管理”无反馈按钮。不拆子状态 | §3.3/3.4 |
| G2 | 草稿选“无效”不触发作废(仅正式提交触发);作废清 owner 不影响审计留痕 | §6.4/6.10 |
| G3 | `lead_history` 全量写 11 种,页面只展示 5 种(领取/释放/反馈/转商机/编辑);VOID 记原持有人 | §6.10 |
| G4 | 定时任务**失效先跑、回收后跑**,两者不再竞争 | §6.7 |
| G5 | 领取/可见按**部门集合(主+兼职)**;与 crm-auth ABAC DEPT 档既有实现对齐(ADR-0005) | §6.2、CONTEXT |
| G6 | 新增自动带**主部门池**不给选;主部门无池→报错拦截 | §6.1 |
| G7 | `hold_limit` +`daily_claim_limit` **分配时也校验** ;额度按被指派人+目标池算;批量部分成功 | §6.3 |
| G8 | 新增§6.0 全局并发原则:状态 CAS 乐观锁,行数=0=失败 | §6.0 |
| G9 | ✅(E) 线索公海**混合展示**(防撞单),展示范围可配置不硬编码 | §7 |
| G10 | 转商机现按同库同事务定,显式写入前提约束;商机独立时重设计为最终一致+补偿 | §6.5、CONTEXT |
| G11 | ✅(B) 不刷 owner_dept_id_snapshot;✅(D) 每日 02:00 全库扫 | §11.1 |
---
## 13. 批量操作与统计接口(v1.2 补全,2026 grill-with-docs 定稿)
**动因**:接口完整性复核发现——原型(A2-1-1/A2-1-4/A7-3-1 + 分配/激活弹窗)大量要求**批量操作**与**顶部统计卡片**,§6 的单条流转已实现但批量与统计接口缺失。本节把缺口 G1–G5 定稿为可实现契约。范围**只含 G1–G5**;G6(导入/导出)/G7(合并)/G8(池导入导出)维持既有「不做 / Out of scope」判定(见 map.md)。
### 13.1 批量操作契约(G1–G4)
**统一语义**:所有批量操作**非原子、逐条 CAS、部分成功**(沿用 §6.0 全局并发原则)。逐条套用对应单条操作的守卫 + CAS + 上限校验;行数=0 或校验不过即记一条失败,不拖垮整批。
**批量接口全集(`-batch` 后缀,与单条动词对称)**:
| 接口 | URL | 入参 | 逐条委派 | 备注 |
|---|---|---|---|---|
| 批量领取 | `POST /api/lead/claim-batch` | `ids: List<Long>` | `claimLead` | 池成员自领,受每日/持有上限 |
| 批量分配到池 | `POST /api/lead/assign-pool-batch` | `ids` , `poolId` | `assignToPool` | 未分发→待领取,不占上限 |
| 批量分配到销售 | `POST /api/lead/assign-user-batch` | `ids` , `userId` | `assignToUser` | 统一分配给同一销售;受被指派人上限 |
| 批量释放 | `POST /api/lead/release-batch` | `ids` | `releaseLead` | 已领取/跟进中→待领取 |
| 批量激活 | `POST /api/lead/activate-batch` | `ids` | `activateLead` | 过期失效→恢复失效前态,重置 N/M |
| 批量删除 | `POST /api/lead/delete-batch` | `ids` | `deleteLead` | 已转商机为删除禁区(记失败) |
| 批量删除公海池 | `POST /api/rule/pool/delete-batch` | `ids` | `deletePool` | 池下有非终态线索则该条失败(池删除保护) |
**批量分配双深度**:前端弹窗选了销售调 `assign-user-batch` ,只选池未选人调 `assign-pool-batch` (对应原型「管理员可以不指定具体销售人员」)。销售人员单选——所有选中线索统一分配给同一 `userId` 。
**统一返回 `Result<BatchResult<LeadBatchFailItem>>` **:
```java
// crm-base/domain/result —— 通用批量结果,对失败项内部结构无感知(保持 crm-base 非业务纯净)
class BatchResult< F > {
int total; // 本次提交条数
int successCount;
int failCount;
List< F > failures; // 逐条失败明细
}
// crm-lead —— 线索域失败项
class LeadBatchFailItem {
Long leadId; // 池批量删除时为 poolId
LeadBatchFailReason reason;
String message;
}
// crm-lead —— 失败原因语义枚举(code 回指既有 ResultCode 65xxx)
enum LeadBatchFailReason {
ALREADY_CONVERTED(65009, "已转商机,不可操作"), // 删除/分配禁区
CONCURRENT_MODIFIED(65010, "已被他人操作"), // CAS 行数=0(抢占分配/被回收失效)
OVER_HOLD_LIMIT(65007, "超出个人持有上限"),
OVER_DAILY_LIMIT(65006, "超出每日领取上限"),
STATUS_NOT_ALLOWED(65003, "当前状态不允许此操作"),
NOT_OWNER(65004, "非本人持有,无权操作");
}
```
前端结果弹窗按 `reason` 聚合展示(原型「分配成功 XX 条 / 失败 XX 条 / 失败类型:已转商机 XX 条,抢占分配 XX 条」)。
### 13.2 统计卡片接口(G5)
**接口**:`POST /api/lead/stats`,入参**复用 `LeadPageParam` **(同一套 viewType + 全部筛选 + `@DataScope` 部门天花板),忽略分页字段。返回 `Result<LeadStatsDTO>` 。
**统计口径 = 当前视图数据集口径,不是全库**:`stats` 与 `page` 吃完全相同的数据集条件(viewType + 筛选 + 数据范围),只把「取一页」换成「按 status 分组计数」。例:`MY_LEAD` 视图的 `total` = 我持有的线索总条数,`claimed` = 我的线索里 status IN(3,4) 的条数。每个视图都有自己的卡片;接口通用、全量返回 5 个计数,前端按需取。
```java
class LeadStatsDTO {
long total; // 数据集内全部
long claimed; // 「已被领取」= status IN (3 已领取, 4 跟进中)——展示文案「已被领取」由前端渲染
long converted; // 已转商机 = status 5
long todayNew; // 今日新增 = DATE(create_time) = CURRENT_DATE(同样受视图+筛选+数据范围约束)
long undistributed; // 未分发 = status 1
}
```
### 13.3 复合卡片下钻:`status` 单值 → `statusIn` 多值(改写既有 LeadPageParam)
**动因**:统计卡片可点击下钻——点卡片把该卡的状态条件塞进 `LeadPageParam` 再调 `/page` 。但「已被领取」是 status IN(3,4) 的**并集**,单值 `Integer status` 表达不了。
**改写**:`LeadPageParam.status`(单值 `Integer` )→ `statusIn` (`List< Integer > `),`/page` 与 `/stats` 共用;列表 SQL 用 `status IN (...)` 。单状态卡传单元素列表,「已被领取」传 `[3,4]` 。原型「全部状态」下拉本就可能多选,一并支持。此改动落在**接口尚未联调/未发文档**阶段,契约变更成本低。
### 13.4 决策清单(v1.2)
| # | 决策 | 出处 |
|---|---|---|
| B1 | 本期只补 G1–G5;G6/G7/G8 维持 Out of scope | §13、map |
| B2 | 批量返回 `Result<BatchResult<F>>` 完整体(total/success/fail/failures) | §13.1 |
| B3 | `BatchResult<F>` 泛型放 crm-base;`LeadBatchFailItem`/`LeadBatchFailReason` 放 crm-lead | §13.1 |
| B4 | 批量接口 `-batch` 后缀;`ids` 表单多值传参 | §13.1 |
| B5 | 批量分配双深度(到池 / 到销售),与单条对称 | §13.1 |
| B6 | `LeadBatchFailReason` 新造语义枚举,带 code 回指 65xxx | §13.1 |
| B7 | `/stats` 复用 LeadPageParam;统计口径随视图(非全库);全量返回前端按需取 | §13.2 |
| B8 | 「已被领取」= status IN(3,4);后端字段 `claimed` ,文案前端渲染 | §13.2 |
| B9 | `LeadPageParam.status` 单值 → `statusIn` 多值,支撑复合卡下钻,page/stats 共用 | §13.3 |