# 线索域数据隔离在既有数据范围机制上的落地 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] 撤销即失效**:硬删授权行。