Files
workspace/code/fms/字段权限与用户个性化设计.md
T
2026-09-09 22:40:13 +08:00

319 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FMS 权限体系、字段权限与数据范围设计
## 0. 总体约定
本系统是 SQL 驱动的内部 ERP,权限表和模块配置表主要用于生成通用查询、写入和界面配置规则。
### 权限层次
| 类别 | 回答的问题 | 存储 | 示例 |
| --- | --- | --- | --- |
| 菜单入口权限 | 能不能显示菜单并进入对应页面 | `s_user_power`(权限点编码) | `menu.cw_fee_list.access` |
| 功能权限 | 进入页面后能不能执行某个动作 | `s_user_power`(权限点编码) | `action.cw_fee.delete` — 能不能删除 |
| 数据范围 | 能对**哪些行**做 | `s_user_data_scope`(拼 SQL 行条件) | 只能删**本部门的** |
| 字段权限 | 能看到 / 改**哪些列** | `s_user_field_permission` | 看不到内部成本列 |
菜单是页面的一部分。菜单入口权限同时控制菜单显示、路由进入和页面初始化接口;没有该权限时,用户不能通过直接输入 URL 进入页面。`create` / `update` / `delete` / `export` 以及 `audit` / `settle` 等业务动作属于功能权限;数据范围只描述行的可见与可操作边界,字段权限只描述列的访问能力。
最终权限是交集关系:
```text
菜单入口权限 ∩ 功能权限 ∩ 数据范围 ∩ 字段权限
```
### 权限编码规范
权限点编码即 `s_power.b_id`,也是 `s_user_power.b_power_id` 的外键值。格式为 `类型.对象[.能力]`,用**点号分隔层级**,全小写:
| 类型 | 编码 | 含义 | 生成时机 |
| --- | --- | --- | --- |
| `menu` | `menu.<菜单编码>.access` | 菜单显示及对应页面进入 | 保存菜单时自动生成 |
| `module` | `module.<模块编码>.access` | 模块数据访问(通常对应 read) | 模块启用时自动生成 |
| `action` | `action.<模块编码>.<操作或动作>` | 数据操作 `create` / `update` / `delete` / `export`,以及业务动作 `audit` / `settle` 等 | 模块启用或动作注册时自动生成 |
约定:
- 点号分隔层级,从左到右依次为 `类型` → `对象` → `能力`;
- **对象编码本身不允许包含 `.`**:模块和菜单编码统一不含点号,否则无法按段解析;
- `_` 只用于编码内部(如 `container_main`),**不做分隔符**;
- 不使用 `/`、`#`、`|`、`:`(与路由冲突或需转义);
- 最长编码为 `action.container_main.cancel_settle`(33 字符),`b_id` 预留 `varchar(100)`;
- 编码生成后**不可编辑**,需要变更时删除重建;
- 判断权限时使用完整编码(前端用 Set 判断,零 join);查询时可用前缀,如 `b_id LIKE 'action.cw_fee.%'` 取某模块全部动作;注意 `_` 在 `LIKE` 中是单字符通配符,前缀查询时需用 `ESCAPE '\'` 转义或改用 `LEFT(b_id, n)` 比较。
权限采用“入口和功能默认拒绝、字段使用模块默认配置并允许用户收紧、数据范围使用模块默认或操作级明确配置”的策略:
- `menu.<菜单编码>.access` 同时表示菜单显示和页面进入,必须存在有效的用户授权记录;不再单独定义 `page.*` 或 `report.*` 入口权限;
- `module.<模块编码>.access` 表示模块访问;模块级数据操作统一编码为 `action.<模块编码>.<操作>`;功能权限没有有效授权记录即无权限,`s_power` 或用户授权记录存在 `b_canuse = 0` 时明确禁用;
- 字段没有有效的用户字段策略时,按模块字段默认配置执行,不额外限制查询、编辑和导出;有效字段策略只能收紧权限;
- 数据范围优先使用操作级 `override`,没有 `override` 再使用模块默认的 `*` 范围;模块必须由 IT 明确配置默认范围,未配置时拒绝访问;同一层级多条范围按 OR 合并;
- 用户个性化只影响列表布局、查询收起态、顺序和宽度,不得越过字段权限、字段启用状态或数据范围;
- 数据库层不建 check 约束,字段取值及组合规则由业务层校验(见各表下方「业务层约束」)。
权限过滤、字段裁剪和数据范围条件最终必须落实到查询 SQL 中,前端隐藏菜单、按钮和字段只用于界面展示。
**当前落地方式(阶段性)**:后端是通用数据网关,`/data/loaddata` 直接接受 `search_condition`(行过滤)与 `search_columns`(列裁剪),自身不做权限判断。因此本阶段由**前端拼装 SQL 条件**实现权限过滤:
- 数据范围 → 拼进 `search_condition`;
- 字段 `hidden` 策略 → 从 `search_columns` 中剔除;
- 拼装逻辑收敛在前端唯一的纯函数工具里(不散落在各页面),后续后端具备能力时可原样下沉;
- 这只是执行位置的前移,不改变任何权限规则;前端拼装属于界面层控制,敏感数据仍应在具备条件后补充后端校验。
### 资源与权限归属
权限以菜单入口和业务模块为核心,不以 Vue 路由或组件为核心:
| 资源 | 主要职责 | 是否授予模块数据权限 |
| --- | --- | --- |
| `menu` | 菜单显示、页面入口和路由访问 | 否 |
| `module` | 模块访问、字段和数据范围;数据操作以 `action.<模块编码>.<操作>` 表示 | 是 |
| `action` | 新增、修改、删除、导出,以及审核、反审核、核销等业务动作 | 通常绑定业务模块 |
| Vue 路由/组件 | 技术实现和页面复用 | 否 |
- 菜单权限不自动授予所使用模块的操作权限;
- 一个菜单页面可以使用多个模块,一个模块可以被多个菜单页面复用;
- Vue 路由和组件不是权限标识,同一公共 Vue 页面可以被多个菜单使用;
- `s_menu.b_module_id` 不能表达多模块关系,应由 `s_menu_module` 维护菜单与模块的多对多关系。
### 复合页面和公共页面
以海运详细页为例:
```text
海运详细页
├── sea_main
├── container_main
├── container_detail
├── finance_main
└── finance_detail
```
技术人员按菜单配置一次“页面使用哪些模块”的关系。页面打开后,各模块分别检查自己的访问权限、字段权限和数据范围:
- 必需模块没有读取权限时,页面不能完成正常业务,可以拒绝进入;
- 非必需模块没有读取权限时,隐藏对应页签或区域;
- 菜单权限与模块权限不自动继承;
- 同一个公共 Vue 页面被多个菜单使用时,以当前菜单编码和实际 `module_id` 判断权限,不以路由名称或组件名称判断权限。
动作权限按业务模块归属配置,统一使用 `action.<module_id>.<action_id>`;不把动作绑定到 Vue 路由或公共组件。确实只对某个菜单页面生效的动作,可在 `s_menu_module` 中配置页面能力上限,再与用户的模块动作权限取交集。
### 权限计算顺序
一个页面或接口按以下顺序计算最终权限:
1. 用户账号和对应资源是否启用;
2. 菜单入口是否有有效 `menu.<菜单编码>.access` 授权;无权限时菜单不显示且页面不可进入;
3. 当前动作是否有有效 `action.<模块>.<动作>` 授权,并且该动作在 `s_menu_module` 中被当前页面允许;
4. 当前模块操作是否被 `s_power` 或 `s_user_power.b_canuse = 0` 明确禁用;无有效授权记录即无权限;
5. 根据模块字段元数据和用户字段策略裁剪字段、查询和导出能力(当前由前端从 `search_columns` 中剔除隐藏列);
6. 根据数据范围的 `override`、`default` 或 `all` 生成 SQL 行条件(当前由前端拼装进 `search_condition`);
7. 在以上结果基础上应用用户列表布局和查询布局偏好。
页面显示权限和接口执行权限必须使用同一套计算规则。前端可以提前隐藏无权限内容,但不能代替后端 SQL 的最终判断。
## 1. 表结构
> 权限相关表的完整 DDL、索引与业务层约束统一维护在《FMS新系统核心表结构设计.md》「8. 权限」;用户个性化偏好表见同文档「4. 用户个性化」。本章只保留设计说明与计算规则。
### 1.1 s_menu_module(菜单-模块关系表)
菜单页面与业务模块是多对多关系。菜单记录既表示导航入口,也表示对应的逻辑页面;Vue 路由和公共组件只作为技术实现,不参与权限标识。
业务层约束:
- `b_menu_code`、`b_module_id` 必须引用已启用的菜单和模块;
- `b_allow_*` 表示 IT 为该页面配置的能力上限,不代表用户已经获得权限;
- 用户最终能力为页面上限与用户功能权限的交集;
- `b_required = 1` 的模块没有访问权限时,页面可以拒绝进入;非必需模块可隐藏对应区域;
- 同一模块可被多个菜单复用,同一菜单也可使用多个模块。
### 1.2 s_power(权限定义表)
定义见《FMS新系统核心表结构设计.md》「8. 权限」。
### 1.3 s_user_power(用户权限授权表)
定义见《FMS新系统核心表结构设计.md》「8. 权限」。
### 1.4 s_user_field_permission(用户字段访问策略表)
定义见《FMS新系统核心表结构设计.md》「8. 权限」。
### 1.5 s_user_data_scope(用户数据范围授权表)
定义见《FMS新系统核心表结构设计.md》「8. 权限」。
数据范围计算规则:
1. 先查询当前操作的 `override` 记录;存在时只使用 `override`,忽略模块默认范围;
2. 没有 `override` 时使用 `operation = '*'` 的 `default` 记录;
3. 默认范围和操作覆盖都没有配置时,按 `all` 处理,不追加数据行过滤条件;
4. 同一层级存在多条范围记录时按 OR 合并;`b_scope_no` 只用于区分同级范围行;
5. `read`、`export`、`update`、`delete` 在 SQL 行条件中应用范围;`create` 在写入前校验新增数据是否符合范围。
### 配置流程
技术人员按以下顺序配置,避免在每个页面重复维护权限:
1. 配置模块、字段和模块默认数据范围;
2. 配置菜单、Vue 模板及 `s_menu_module` 菜单-模块关系;
3. 由系统按菜单、模块和动作定义生成 `s_power`;
4. 在用户授权界面按菜单展示入口、模块动作、字段限制和数据范围;
5. 客户通常只需要勾选可进入的菜单和可执行的动作,再配置敏感字段和数据范围限制。
客户按菜单授权,不需要理解路由或 Vue 组件。菜单下展示该页面使用的模块和可用操作,用户只保存明确授予的 `s_user_power.b_canuse = 1` 记录;字段和数据范围保存用户级限制或操作级范围配置。
菜单与模块的多对多关系由 `s_menu_module` 维护。菜单页面使用同一模块时,模块定义只维护一次;页面允许的操作由关系表的 `b_allow_*` 配置限制。
当前不单独引入 `s_feature`:菜单记录已经承担逻辑页面入口的职责,Vue 公共页面通过菜单的模板配置复用;只有将来出现脱离菜单的可授权功能或需要独立维护的页面区域时,才重新评估是否增加功能实例对象。
### 1.6 s_user_field_pref(非权限:用户列表布局偏好表)
定义见《FMS新系统核心表结构设计.md》「4. 用户个性化」。
### 1.7 s_user_query_field_pref(非权限:用户查询布局偏好表)
定义见《FMS新系统核心表结构设计.md》「4. 用户个性化」。
用户个性化规则:
- `s_user_field_pref` 区分查看列表和可编辑列表两套布局;
- `s_user_query_field_pref` 只影响普通查询收起态,展开态和高级查询仍使用所有有效查询字段;
- 没有个人偏好时,回落到模块默认的 `s_field_view`、`s_field_edit`、`s_field_query` 配置;
- 个人偏好只能调整可见字段的显示、顺序和宽度,不能显示隐藏字段、启用停用字段或扩大查询/导出权限;
- “恢复默认”删除当前用户、当前模块对应的个人偏好记录。
---
## 2. Demo 数据
示例沿用 `cw_fee` 费用模块。示例用户:张三(费用会计)、李四(财务经理)。
示例菜单与模块关系:
| b_menu_code | b_module_id | b_usage_type | b_required | b_allow_read | b_allow_create | b_allow_update | b_allow_delete | b_allow_export |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| sea_fee_list | cw_fee | main | 1 | 1 | 0 | 0 | 0 | 1 |
| sea_fee_edit | cw_fee | main | 1 | 1 | 1 | 1 | 0 | 0 |
费用列表和费用编辑使用同一个 `cw_fee` 模块,但页面能力上限不同;用户最终能力还要与自己的功能权限取交集。
### 2.1 s_power(权限定义)
| b_id | b_name | b_power_type | b_owner_type | b_owner_id | b_capability | b_operation | b_canuse | b_xh |
| --- | --- | --- | --- | --- | --- | --- | ---: | ---: |
| menu.cw_fee_list.access | 费用列表菜单 | menu | menu | cw_fee_list | access | access | 1 | 10 |
| menu.cw_fee_edit.access | 费用编辑菜单 | menu | menu | cw_fee_edit | access | access | 1 | 20 |
| module.cw_fee.access | 费用模块访问 | module | module | cw_fee | access | access | 1 | 30 |
| action.cw_fee.create | 费用新增 | action | module | cw_fee | create | execute | 1 | 40 |
| action.cw_fee.update | 费用修改 | action | module | cw_fee | update | execute | 1 | 50 |
| action.cw_fee.delete | 费用删除 | action | module | cw_fee | delete | execute | 1 | 60 |
| action.cw_fee.export | 费用导出 | action | module | cw_fee | export | execute | 1 | 70 |
| action.cw_fee.audit | 费用审核 | action | module | cw_fee | audit | execute | 1 | 80 |
| action.cw_fee.unaudit | 费用反审核 | action | module | cw_fee | unaudit | execute | 1 | 90 |
### 2.2 s_user_power(用户授权)
| b_user_id | b_power_id | b_canuse | b_updatedatetime |
| --- | --- | ---: | --- |
| zhangsan | menu.cw_fee_list.access | 1 | 2026-02-01 09:00:00 |
| zhangsan | menu.cw_fee_edit.access | 1 | 2026-02-01 09:00:00 |
| zhangsan | module.cw_fee.access | 1 | 2026-02-01 09:00:00 |
| zhangsan | action.cw_fee.update | 1 | 2026-02-01 09:00:00 |
| zhangsan | action.cw_fee.export | 1 | 2026-02-01 09:00:00 |
| zhangsan | action.cw_fee.audit | 1 | 2026-02-01 09:00:00 |
| lisi | menu.cw_fee_list.access | 1 | 2026-01-15 10:00:00 |
| lisi | menu.cw_fee_edit.access | 1 | 2026-01-15 10:00:00 |
| lisi | module.cw_fee.access | 1 | 2026-01-15 10:00:00 |
| lisi | action.cw_fee.create | 1 | 2026-01-15 10:00:00 |
| lisi | action.cw_fee.update | 1 | 2026-01-15 10:00:00 |
| lisi | action.cw_fee.delete | 1 | 2026-01-15 10:00:00 |
| lisi | action.cw_fee.export | 1 | 2026-01-15 10:00:00 |
| lisi | action.cw_fee.audit | 1 | 2026-01-15 10:00:00 |
| lisi | action.cw_fee.unaudit | 1 | 2026-01-15 10:00:00 |
张三没有 `action.cw_fee.create` / `action.cw_fee.delete` 权限。由于统一采用默认拒绝策略,没有授权记录即无权限,不需要额外保存禁止记录。
### 2.3 s_user_field_permission(字段访问策略)
| b_user_id | b_module_id | b_field | b_access_mode | b_allow_query | b_allow_export | b_canuse |
| --- | --- | --- | --- | ---: | ---: | ---: |
| zhangsan | cw_fee | mx_amount | view | 1 | 0 | 1 |
| zhangsan | cw_fee | mx_internal_cost | hidden | 0 | 0 | 1 |
| lisi | cw_fee | mx_amount | edit | 1 | 1 | 1 |
| lisi | cw_fee | mx_internal_cost | view | 1 | 0 | 1 |
张三对金额字段是“可见、只读、可查询、不可导出”,对内部成本字段完全不可见;李四可以编辑和导出金额,但内部成本字段仍然只读。
未配置用户字段策略的字段按模块默认配置执行,不额外限制查询、编辑和导出;上表中的记录只表示对特定字段的收紧。
### 2.4 s_user_data_scope(用户数据范围授权)
| b_user_id | b_module_id | b_operation | b_scope_level | b_scope_type | b_scope_field | b_scope_value | b_scope_no | b_updatedatetime |
| --------- | ----------- | ----------- | ------------- | ---------------- | --------------- | ------------- | ---------- | ------------------- |
| zhangsan | cw_fee | * | default | department | b_department_id | null | 1 | 2026-02-01 09:00:00 |
| zhangsan | cw_fee | update | override | own | b_inputuser_id | null | 1 | 2026-02-01 09:00:00 |
| lisi | cw_fee | * | default | department_tree | b_department_id | null | 1 | 2026-01-15 10:00:00 |
| lisi | cw_fee | delete | override | all | null | null | 1 | 2026-01-15 10:00:00 |
- 张三使用模块默认范围(`*` + department)读取本部门数据,update 有操作级覆盖(override + own),只能修改本人录入的费用;
- 李四使用默认范围 department_tree 处理一般操作,delete 有操作级覆盖为 all,可删全模块数据。
---
## 3. 权限在哪里配置
权限分「定义端」和「授权端」,两者分离:
| 位置 | 配置内容 | 写入的表 |
| --- | --- | --- |
| **模块管理(IT)** | 维护业务模块、字段默认配置和默认数据范围;自动生成 `module.<模块编码>.access` 与 `action.<模块编码>.<操作或动作>` | `s_module`、`s_field`、`s_power` |
| **菜单管理(IT)** | 维护菜单层级、Vue 模板和菜单-模块关系;保存菜单时自动生成 `menu.<菜单编码>.access` | `s_menu`、`s_menu_module`、`s_power` |
| **用户授权(客户)** | 按菜单树勾选入口权限;在菜单下查看其模块并配置功能动作、字段策略和数据范围 | `s_user_power`、`s_user_field_permission`、`s_user_data_scope` |
| **个人偏好(非权限)** | 列可见 / 顺序 / 宽度、查询收起态字段 | `s_user_field_pref`、`s_user_query_field_pref` |
规则:
- 动作归属**模块**而非菜单,同一个动作只配置一次,不随菜单重复;
- 勾选菜单只授予入口权限,不自动授予模块操作权限;
- 客户授权界面按菜单展示模块,但落库仍分别写入菜单权限、模块动作权限、字段权限和数据范围;
- `s_menu_module.b_allow_*` 是 IT 配置的页面能力上限,用户实际能力取其与 `s_user_power` 的交集;
- 菜单 `b_canuse = 0` 时一票否决,所有用户均不可见。
> 现状提示:当前系统只有菜单管理维护权限点字典,`s_user_power` 等授权表尚未建立,也没有授权界面;权限过滤目前只有登录态校验,其余全部未落地。
### 客户授权页面
客户只按菜单树授权,不需要理解 Vue 路由、公共组件或数据库表。选中一个菜单后,右侧展示该菜单通过 `s_menu_module` 使用的模块,并提供:
```text
菜单入口:显示菜单、进入页面
模块能力:查看、新增、修改、删除、导出、业务动作
字段策略:隐藏、只读、可编辑、查询、导出
数据范围:本人、部门、部门树、自定义等
```
勾选菜单只授予入口权限;模块动作、字段权限和数据范围按当前菜单下的模块分组展示,但按各自的表独立落库。
### IT 统一配置页面
IT 在统一配置中心维护模块、字段、动作、菜单模板和 `s_menu_module` 关系:
1. 维护业务模块及字段默认配置;
2. 维护菜单层级、对应 Vue 模板和页面使用的模块;
3. 为模块注册新增、修改、删除、导出、审核等动作;
4. 配置每个菜单-模块关系的必需性和页面能力上限;
5. 由系统自动生成 `s_power`,客户授权页面只消费这些定义。
## 4. 实施约定
1. 入口和功能权限统一采用默认拒绝策略:没有有效授权记录即无权限;权限点或授权记录为 `b_canuse = 0` 时明确禁用。
2. 模块必须配置默认字段策略和默认数据范围;字段用户配置只允许进一步收紧,数据范围按模块默认或操作级明确配置计算。
3. `own` / `department` / `department_tree` 等数据范围所需的用户部门信息,必须由登录用户资料或后端用户服务提供。
4. 当前阶段如果只能由前端拼装 `search_condition` 和 `search_columns`,必须将逻辑集中在唯一工具中;后端或数据网关具备能力后,应把菜单、功能、字段和数据范围校验下沉到服务端。
5. 角色是否作为用户授权的批量模板另行设计,不改变本文件的菜单-模块-字段-数据范围模型。
## 5. 用户个性化(非权限,附录)
`s_user_field_pref` 和 `s_user_query_field_pref` 只保存列表布局和查询布局偏好,不参与权限计算。它们只能在已经通过菜单、功能、字段和数据范围校验后生效,不能显示被字段权限隐藏的字段,也不能扩大查询、导出或数据范围。