# 线索域数据隔离在既有数据范围机制上的落地
Type: grilling
Status: resolved
Resolved: 2026-08-13
## Question
三类角色(销售/部门负责人/超管)对线索、公海池的可见范围与操作权差异极大,全域横切。需定清如何用既有机制(`DataScopeEnum` + `DataScopeInterceptor` + `SysRoleDataScope` + ADR-0018 按模块可配)表达,而非新建机制。
需在本 ticket 定清:
1. **线索模块暴露的权限点** (`clue:*`)清单:公海查看/领取/关注、我的线索 CRUD、线索管理(分配/合并/删除/导入导出)、线索设置(公海池 CRUD/导入导出)分别对应哪些权限点。参考 crm-dict 的 `dict:*` 做法。
2. **数据范围维度** :线索的数据范围按什么过滤?
3. **公海池(crm-rule 侧)的数据范围** :部门负责人只见本部门池、超管全见——同样套 `DataScopeEnum` 还是另有规则。
4. **ADR-0018 的"按模块可配"如何应用于 crm-lead / crm-rule** :这两个模块是否各自注册为一个可配数据范围模块。
产出:三机制权限模型规格 + 可被管控点位候选清单 + 数据范围规则表。
---
## Answer
### 1. 权限模型总览:RBAC + ABAC + ReBAC 三层
线索域的访问控制由**三层正交机制**组合,学名对应 RBAC + ABAC + ReBAC:
```
能否执行某动作 = RBAC(功能权限,已录入权限点则校验,未录入则放行)
能否看到某条数据 = ABAC(页面业务语义 AND 部门数据范围天花板)
OR ReBAC(行级授权通行证,仅越过部门天花板)
```
三层**彼此正交、互不替代**,必须分开实现:
| 层 | 学名 | 跟着谁走 | 实现方式 | 开关 |
|---|---|---|---|---|
| 功能权限 | RBAC | 角色 | 权限点管理录入后生效 | 配了才管,未配放行 |
| 数据可见范围 | ABAC | 当前用户代入规则 | 页面业务语义(service 写死)+ 部门天花板(`DataScopeInterceptor` 自动注入) | always-on |
| 行级授权 | ReBAC | 具体人 + 具体资源 | `sys_row_grant` 通用表 + 拦截器 `OR EXISTS` 旁路 | 有授权关系才生效 |
---
### 2. 机制一:功能权限(RBAC)
功能权限控制"能不能点某按钮 / 调某接口",与"能作用到哪些行"无关。
- **present-then-enforce**:通过权限点管理录入后才校验;未录入则放行,不是代码里预定义固定清单。
- 每个权限点录两样:**前端按钮编码**(控按钮显隐/可点)+ **后端 API URL** (控接口调用)。
- 本 ticket **不产出硬编码权限编码表** ,产出"可被管控点位候选清单"(见第 5 节),管理员按需择录。
---
### 3. 机制二:数据可见范围(ABAC)
ABAC 包含两块,都是"按属性算、不逐条落表":
#### 3.1 第一层——页面业务语义(service 显式写死)
每个视图在 service 层显式加 WHERE,定义"这个页面展示哪一类数据":
| 视图 | 第一层业务条件 |
|---|---|
| 线索公海 | `status = '待领取' AND pool_id IN (当前用户够得着的池)` |
| 我的线索 | `owner_id = 当前用户 AND status IN ('已领取','跟进中')` |
| 我的关注 | `id IN (SELECT lead_id FROM lead_follow WHERE user_id = 当前用户)` |
| 线索管理 | 按筛选项,无固定业务条件 |
**关键规则**:
- "仅本人"的收窄**完全靠第一层** `owner_id = 我` ,不依赖第二层部门天花板。
- "我的关注"**不走行级授权**(见第 4 节 [F]);`id IN 关注表` 本身就是白名单,但仍受部门天花板约束——可见性每请求按当前部门实时算,防止"人事误操作→关注→改回部门→长期越权"。
#### 3.2 第二层——部门数据范围天花板(DataScopeInterceptor 自动注入)
- `lead` 表挂 ** `@DataScope(module = "lead", deptColumn = "dept_id")` **,拦截器自动往 SELECT 注入 `dept_id IN (...)` 或不注入(ALL)。
- `lead_pool` 表(crm-rule 侧)挂 ** `@DataScope(module = "lead", deptColumn = "dept_id")` **,复用同一模块配置(K-1a),部门负责人只见本部门池、超管全见。
- **`lead.dept_id` 含义**:恒等于该线索所属公海池的归属部门,全生命周期**不变动**([I] 已定)。领取后 dept_id 不跟领取人主部门走——因为销售只能领取本部门公海池线索,故领取人与池必同部门,结果等价。
档位折算(按角色在 `lead` 模块配置,多角色取最宽):
| 典型角色 | 推荐档位 | 说明 |
|---|---|---|
| 销售 | DEPT(本部门) | 默认;管理员可按 ADR-0008 调宽 |
| 部门负责人 | DEPT_AND_CHILD(本部门及下属) | 覆盖下属部门公海+私海 |
| 超级管理员 | ALL(全部) | 不注入部门条件 |
**两层叠加不冲突**:三视图查同一张 lead 表,第二层注入的都是同一角色档位的 `dept_id IN(...)` (或 ALL),不打架;"我的 vs 公海"的区分完全由第一层业务条件承担。
---
### 4. 机制三:行级授权(ReBAC)
#### 4.1 定位
ReBAC 是**加法性质的通行证**,只越过机制二的部门天花板,不改页面业务语义:
- 李四被设为 B 部门公海池的协作人 → 他在"线索公海"页能看到该池线索(越过部门天花板)。
- 但李四**不会**因此出现在"我的线索"页——那页语义是 `owner_id = 李四` ,李四不是 owner,通行证不改这一条。
- 通行证**只放行 SELECT**(`DataScopeInterceptor` 只拦截 SELECT);写权由权限点 + service 层 owner/状态机判定,与通行证无关。
#### 4.2 触发动作与授权颗粒度(PRD-1,产品已确认)
| 触发动作 | 授权颗粒度 | resource_type | 线索域翻译 |
|---|---|---|---|
| 将用户设为公海池**负责人** | 池维度 | `pool` | 该池下全部线索可见(间接授权) |
| 将用户设为公海池**协作人** | 池维度 | `pool` | 该池下全部线索可见(间接授权) |
| 将用户加入商机**团队成员** | 单条 | `opportunity` | 该条商机可见(直接授权) |
| 将用户加入客户**团队成员** | 单条 | `customer` | 该条客户可见(直接授权) |
| 将用户加入项目**团队成员** | 单条 | `project` | 该条项目可见(直接授权) |
**线索侧两项决策**([D][E][F] 已定):
- **[D]** 线索**不开** `resource_type = lead` 单条直接授权通道。分配/指派 = 改 `owner_id` ,走第一层业务过滤,不发通行证。
- **[E]** 池配置管理**不发通行证**。"能不能改某个池配置" = 功能权限(菜单有没有配)× 数据范围(池 dept_id ∈ 用户部门集合)。
- **[F]** 线索关注**不走行级授权**。"我的关注" = `id IN 关注表` AND 部门天花板,实时按当前部门算。
#### 4.3 权限边界(PRD-2,产品已确认)
**只读**。通行证只放行"看得到";看到之后能不能操作(反馈/转商机/编辑/删除),由各自 service 层按 owner / 状态机 / 功能权限判定,通行证不授予任何写权。
#### 4.4 撤销行为(PRD-3,产品已确认)
移出协作人/团队成员名单**那一刻即失效**:下一个请求的 `OR EXISTS` 旁路命中不到。授权行**硬删**(`DELETE FROM sys_row_grant WHERE user_id = ? AND resource_type = ? AND resource_id = ?`),不做软删 + 失效标记。
依据:`DataScopeInterceptor` 在 `prepare` 阶段实时查,天然立即失效,零额外成本;"谁何时被加进/移出"由 lead_history 操作日志留痕,不靠授权表本身。
#### 4.5 实现说明(超出 crm-lead 边界,需独立 ticket)
行级授权为**平台级通用能力**,crm-auth 需新增:
- **`sys_row_grant` 表**:`(id, user_id, resource_type, resource_id, created_by, created_at)`。按 `(user_id, resource_type, resource_id)` 建唯一索引,数据稀疏,不影响列表查询性能。
- **拦截器 `OR EXISTS` 旁路**:在 `DataScopeInterceptor` 的 `buildCondition` 里,对声明了行级授权来源的表,将 WHERE 改写为:
```sql
(dept_id IN (...) OR EXISTS (
SELECT 1 FROM sys_row_grant
WHERE user_id = #{currentUserId}
AND resource_type = #{translatedType}
AND resource_id = #{translatedId}
))
```
池维度授权(`resource_type = 'pool'`)在翻译层展开为"该池下全部 lead_id"(`pool_id = ?`),无需逐条落表。
- **`@DataScope` 注解扩展**:增加 `rowGrantSource` 属性,声明该表的行级授权来源(`resource_type` + 关联列),供拦截器翻译。
---
### 5. 可被管控点位候选清单(RBAC,供权限点管理录入)
管理员**择需录入**;此处仅为"有哪些点位可纳管"的备选表,不是运行期一定生效的清单。编码格式(前端按钮编码 + API URL)在权限点管理录入时由管理员给定,spec 不预置固定编码串。
"部门负责人只管本部门 / 超管管全部"这类部门收窄**由机制二(数据范围)自动完成**,不靠功能权限点表达。
**crm-lead 候选点位**:
| 分组 | 候选动作 |
|---|---|
| 线索公海 | 查看列表、领取、关注/取关 |
| 我的线索 | 查看列表、新增、编辑、反馈(含暂存/附件上传)、转商机、释放 |
| 我的关注 | 查看列表、取关 |
| 线索管理 | 页面访问、分配/批量分配、单条/批量删除、导入、导出、回收任意私海线索、激活失效线索(状态机 F2) |
**crm-rule 候选点位**(公海池配置):
| 分组 | 候选动作 |
|---|---|
| 线索设置-公海池 | 查看列表、新增、编辑(含负责人/协作人管理)、删除、导入、导出 |
---
### 6. 数据范围规则汇总(角色 × 视图 × 过滤维度)
| 视图 | 第一层(业务语义,service 写死) | 第二层(部门天花板,拦截器注入) | ReBAC 旁路 |
|---|---|---|---|
| 线索公海 | `status='待领取' AND pool_id IN (够得着的池)` | `dept_id IN (本部门集)` | 池负责人/协作人:池维度 grant 翻译为 `pool_id=?` |
| 我的线索 | `owner_id=我 AND status IN (已领取,跟进中)` | `dept_id IN (本部门集)` | 无(owner=我,天花板不额外收窄) |
| 我的关注 | `id IN (关注表)` | `dept_id IN (本部门集)` | 无(关注不走 grant,[F]) |
| 线索管理 | 按筛选项 | `dept_id IN (本部门集)` | 同公海 |
| 公海池列表(crm-rule) | 无固定条件 | `dept_id IN (本部门集)` (复用 lead 模块) | 无(池配置管理不发通行证,[E]) |
> 超管档位 = ALL,第二层不注入任何条件,直接全量。
---
## 已 grill 定稿的核心模型(grill 过程记录)
### 数据隔离两层叠加
**最终 WHERE = 第一层业务过滤 AND 第二层数据范围**,分工如下:
#### 第一层 —— 业务过滤(service 显式 SQL,定"哪个视图")
- "我的线索" = `owner_id = 当前用户 AND status IN (…)`
- "我的关注" = `id IN (当前用户关注表)`
- "线索公海" = `status = 待领取 AND 公海池 ∈ 当前用户够得着的池`
- "线索管理" = 治理视图,业务条件按筛选项
- **"仅本人"的收窄完全靠第一层 `owner_id = 我` ,不依赖第二层。**
#### 第二层 —— 数据范围(DataScopeInterceptor 自动 AND,按角色档位)
- **lead 表只挂一个** `@DataScope(module = "lead", deptColumn = "dept_id")` ,**只按 `dept_id` 做部门档兜底,不使用 owner 列**(owner 过滤下放到第一层)。
- 档位折算:销售 → DEPT;部门负责人 → DEPT_AND_CHILD;超管 → ALL。
- **`dept_id` 存"公海池的归属部门"**,全生命周期不变。
### 已定稿决策
- **[I] 线索 `dept_id` 恒定、永不变动**(选 I-1)。
- **[K] 公海池表复用 `lead` 模块**(选 K-1 + K-1a)。
- **[D] 线索不开单条直接授权通道**(产品确认)。
- **[E] 池配置管理不发通行证**(产品确认)。
- **[F] 关注不走行级授权**(产品确认)。
- **[PRD-1] 触发动作**:团队成员(商机/客户/项目,逐条)+ 公海池负责人/协作人(线索,池维度)。
- **[PRD-2] 只读**:通行证只放行 SELECT,写权由权限点 + service 判。
- **[PRD-3] 撤销即失效**:硬删授权行。