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

311 lines
16 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,权限表和模块配置表主要用于生成通用查询、写入和界面配置规则。权限采用“入口默认拒绝、其它能力默认放行、用户配置负责收紧”的策略:
- `menu/access` 和 `action/execute` 属于入口或动作权限,必须存在有效的用户授权记录;`page/access`、`report/access` 仅在页面或报表需要独立入口控制时启用,启用后同样必须存在有效授权记录;
- `module/read|create|update|delete|export` 未配置用户授权时默认放行;`s_power` 权限点或用户授权记录存在 `b_canuse = 0` 时明确禁用;
- 字段没有有效的用户字段策略时,按模块字段默认配置执行,不额外限制查询、编辑和导出;有效字段策略只能收紧权限;
- 数据范围没有配置时按 `all` 处理;有配置时先使用操作级 `override`,没有 `override` 再使用模块默认的 `*` 范围;同一层级多条范围按 OR 合并;
- 用户个性化只影响列表布局、查询收起态、顺序和宽度,不得越过字段权限、字段启用状态或数据范围;
- 数据库层不建 check 约束,字段取值及组合规则由业务层校验(见各表下方「业务层约束」)。
权限过滤、字段裁剪和数据范围条件最终必须在后端 SQL 中落实,前端隐藏菜单、按钮和字段只用于界面展示。
### 资源与权限归属
权限以业务模块为核心,不以菜单或 Vue 路由为核心:
| 资源 | 主要职责 | 是否授予模块数据权限 |
| --- | --- | --- |
| `menu` | 导航入口、菜单显示 | 否 |
| `page` | 逻辑页面访问(按需启用) | 否 |
| `module` | 读取、新增、修改、删除、导出、字段和数据范围 | 是 |
| `action` | 审核、反审核、核销等具体业务动作 | 通常绑定业务模块 |
| Vue 路由/组件 | 技术实现和页面复用 | 否 |
- 菜单或页面权限不会自动授予所使用模块的权限;
- 一个页面可以使用多个模块,一个模块可以被多个页面复用,模块权限始终按模块自身计算;
- Vue 路由和组件不是稳定的权限标识。`page` 权限使用稳定的逻辑页面编码,不使用路由路径或组件名称;
- `s_menu.b_module_id`(如果保留)只表示菜单的默认/主模块,不能表示页面使用的全部模块,也不能触发权限继承;
- 普通菜单页面通常只配置菜单入口权限。只有需要独立直接访问的页面,才额外定义 `page/access` 权限,避免客户重复勾选菜单和页面。
### 复合页面和公共页面
以海运详细页为例:
```text
海运详细页
├── sea_main
├── container_main
├── container_detail
├── finance_main
└── finance_detail
```
技术人员只需配置一次“页面使用哪些模块”的关系。页面打开后,各模块分别检查自己的 `read`、字段权限和数据范围:
- 必需模块没有读取权限时,页面不能完成正常业务,可以拒绝进入;
- 非必需模块没有读取权限时,隐藏对应页签或区域;
- 页面权限、菜单权限和模块权限互不继承;
- 同一个公共 Vue 页面被多个模块使用时,以当前页面配置和实际 `module_id` 判断权限,不以路由名称判断权限。
动作权限按业务归属配置:有明确业务主模块时使用 `action.module.<module_id>.<action_id>.execute`;真正跨模块且没有主模块的页面流程才使用 `action.page.<page_id>.<action_id>.execute`;不把动作绑定到 Vue 路由或公共组件。
### 权限计算顺序
一个页面或接口按以下顺序计算最终权限:
1. 用户账号和对应资源是否启用;
2. 菜单入口是否有有效授权;存在独立页面/报表权限时,再检查对应入口授权;
3. 当前动作是否有有效 `action/execute` 授权;
4. 当前模块操作是否被 `s_power` 或 `s_user_power.b_canuse = 0` 明确禁用,未配置时按默认放行;
5. 根据模块字段元数据和用户字段策略裁剪字段、查询和导出能力;
6. 根据数据范围的 `override`、`default` 或 `all` 生成 SQL 行条件;
7. 在以上结果基础上应用用户列表布局和查询布局偏好。
页面显示权限和接口执行权限必须使用同一套计算规则。前端可以提前隐藏无权限内容,但不能代替后端 SQL 的最终判断。
## 1. 表结构
### 1.1 s_power(权限定义表)
```sql
create table dbo.s_power (
b_id bigint not null, -- 内部主键,雪花/序列生成,不表达业务含义
b_code varchar(300) not null, -- 稳定、全局唯一的业务键,由系统生成
-- 示例:menu.cw_fee.access / module.cw_fee.read / action.module.cw_fee.audit.execute
b_name nvarchar(200) not null,
b_i18n varchar(150) null,
b_power_type varchar(20) not null, -- menu/page/report/module/action
b_resource_id varchar(128) not null, -- 当前权限对应的资源编码
b_owner_type varchar(20) null, -- action 的 owner 类型:menu/page/module
b_owner_id varchar(128) null, -- action 的 owner 编码
b_capability varchar(30) not null, -- access/execute/read/create/update/delete/export
b_canuse tinyint not null default 1,
b_xh int not null default 0,
constraint PK_s_power primary key (b_id),
constraint UQ_s_power_code unique (b_code)
);
create index IX_s_power_resource
on dbo.s_power (b_power_type, b_owner_type, b_owner_id, b_resource_id, b_canuse);
```
业务层约束(数据库不建 check 约束,由业务层校验):
- `b_power_type` 只能取 `menu / page / report / module / action`;
- `menu / page / report` 类型:`b_owner_type`、`b_owner_id` 必须为空,`b_capability` 固定为 `access`;
- `module` 类型:`b_owner_type`、`b_owner_id` 必须为空,`b_capability` 只能取 `read / create / update / delete / export`;
- `action` 类型:`b_owner_type` 只能取 `menu / page / module`,`b_owner_id` 必填,`b_capability` 固定为 `execute`。
### 1.2 s_user_power(用户权限授权表)
```sql
create table dbo.s_user_power (
b_user_id varchar(50) not null,
b_power_id bigint not null, -- 引用 s_power.b_id(内部主键)
b_canuse tinyint not null default 1,
b_updatedatetime datetime2 null,
constraint PK_s_user_power primary key (b_user_id, b_power_id)
);
create index IX_s_user_power_user
on dbo.s_user_power (b_user_id, b_canuse, b_power_id);
```
### 1.3 s_user_field_permission(用户字段访问策略表)
```sql
create table dbo.s_user_field_permission (
b_user_id varchar(50) not null,
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_access_mode varchar(10) not null, -- hidden / view / edit
b_allow_query tinyint not null default 0,
b_allow_export tinyint not null default 0,
b_canuse tinyint not null default 1,
b_updatedatetime datetime2 null,
constraint PK_s_user_field_permission
primary key (b_user_id, b_module_id, b_field)
);
create index IX_s_user_field_permission_module
on dbo.s_user_field_permission (b_user_id, b_module_id, b_canuse, b_field);
```
业务层约束(数据库不建 check 约束,由业务层校验):
- `b_access_mode` 只能取 `hidden / view / edit`;
- `b_access_mode = 'hidden'` 时,`b_allow_query` 和 `b_allow_export` 必须同时为 `0`;
- 没有 `b_canuse = 1` 的有效记录时,该字段按模块字段默认配置执行,不视为 `hidden`。
### 1.4 s_user_data_scope(用户数据范围授权表)
```sql
create table dbo.s_user_data_scope (
b_user_id varchar(50) not null,
b_module_id varchar(50) not null,
b_operation varchar(30) not null,
-- * / read / create / update / delete / export
-- * 表示模块默认范围,仅 b_scope_level = 'default' 时允许使用
b_scope_level varchar(10) not null default 'default',
-- default / override;default 使用 b_operation='*',override 为操作级覆盖
b_scope_no int not null default 1,
b_scope_type varchar(30) not null,
-- own / department / department_tree / all / custom
b_scope_field varchar(50) null,
b_scope_value varchar(100) null,
b_updatedatetime datetime2 null,
primary key (b_user_id, b_module_id, b_operation, b_scope_level, b_scope_no)
);
create index IX_s_user_data_scope_module
on dbo.s_user_data_scope (b_user_id, b_module_id, b_operation, b_scope_level, b_scope_type);
```
数据范围计算规则:
1. 先查询当前操作的 `override` 记录;存在时只使用 `override`,忽略模块默认范围;
2. 没有 `override` 时使用 `operation = '*'` 的 `default` 记录;
3. 默认范围和操作覆盖都没有配置时,按 `all` 处理,不追加数据行过滤条件;
4. 同一层级存在多条范围记录时按 OR 合并;`b_scope_no` 只用于区分同级范围行;
5. `read`、`export`、`update`、`delete` 在 SQL 行条件中应用范围;`create` 在写入前校验新增数据是否符合范围。
### 配置流程
技术人员按以下顺序配置,避免在每个页面重复维护权限:
1. 配置模块、字段和模块默认布局;
2. 配置菜单、逻辑页面及页面使用的模块关系;
3. 由系统按菜单、页面、模块和动作定义生成 `s_power`;
4. 在用户授权界面按入口、动作、模块限制、字段限制、数据范围分组展示;
5. 客户通常只需要勾选可进入的菜单和可执行的动作,再配置少量模块禁用、敏感字段和数据范围限制。
客户不需要为每个页面重复勾选 `module.read`、`module.update` 等权限。模块操作默认放行,只有禁止某个操作时才保存 `s_user_power.b_canuse = 0`;字段和数据范围也只保存需要收紧的例外配置。
页面与模块的多对多关系属于系统配置数据,建议由独立的页面模块关系配置维护。该关系表的最终结构在《FMS新系统核心表结构设计.md》统一整理前,本文件只约定其业务语义,不在本文件重复定义核心表。
### 1.5 s_user_field_pref(用户列表布局偏好表)
```sql
create table dbo.s_user_field_pref (
b_user_id varchar(50) not null,
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_mode varchar(10) not null, -- view / edit
b_visible tinyint not null default 1,
b_xh int not null default 0,
b_width int null,
b_updatedatetime datetime2 null,
primary key (b_user_id, b_module_id, b_field, b_mode)
);
create index IX_s_user_field_pref_order
on dbo.s_user_field_pref (b_user_id, b_module_id, b_mode, b_xh, b_field);
```
### 1.6 s_user_query_field_pref(用户查询布局偏好表)
```sql
create table dbo.s_user_query_field_pref (
b_user_id varchar(50) not null,
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_quick_visible tinyint not null default 1,
b_xh int not null default 0,
b_updatedatetime datetime2 null,
primary key (b_user_id, b_module_id, b_field)
);
create index IX_s_user_query_field_pref_order
on dbo.s_user_query_field_pref (b_user_id, b_module_id, b_xh, b_field);
```
用户个性化规则:
- `s_user_field_pref` 区分查看列表和可编辑列表两套布局;
- `s_user_query_field_pref` 只影响普通查询收起态,展开态和高级查询仍使用所有有效查询字段;
- 没有个人偏好时,回落到模块默认的 `s_field_view`、`s_field_edit`、`s_field_query` 配置;
- 个人偏好只能调整可见字段的显示、顺序和宽度,不能显示隐藏字段、启用停用字段或扩大查询/导出权限;
- “恢复默认”删除当前用户、当前模块对应的个人偏好记录。
---
## 2. Demo 数据
示例沿用 `cw_fee` 费用模块。示例用户:张三(费用会计)、李四(财务经理)。
### 2.1 s_power(权限定义)
| b_id | b_code | b_name | b_power_type | b_resource_id | b_owner_type | b_owner_id | b_capability | b_canuse | b_xh |
| ---: | --- | --- | --- | --- | --- | --- | --- | ---: | ---: |
| 1001 | menu.cw_fee.access | 费用管理菜单 | menu | cw_fee | null | null | access | 1 | 10 |
| 1002 | page.cw_fee.access | 费用页面 | page | cw_fee | null | null | access | 1 | 20 |
| 1003 | action.module.cw_fee.audit.execute | 费用审核 | action | audit | module | cw_fee | execute | 1 | 30 |
| 1004 | action.module.cw_fee.unaudit.execute | 费用反审核 | action | unaudit | module | cw_fee | execute | 1 | 40 |
| 1005 | module.cw_fee.read | 费用读取 | module | cw_fee | null | null | read | 1 | 50 |
| 1006 | module.cw_fee.create | 费用新增 | module | cw_fee | null | null | create | 1 | 60 |
| 1007 | module.cw_fee.update | 费用修改 | module | cw_fee | null | null | update | 1 | 70 |
| 1008 | module.cw_fee.delete | 费用删除 | module | cw_fee | null | null | delete | 1 | 80 |
| 1009 | module.cw_fee.export | 费用导出 | module | cw_fee | null | null | export | 1 | 90 |
| 1010 | report.cw_fee_summary.access | 费用汇总报表 | report | cw_fee_summary | null | null | access | 1 | 100 |
### 2.2 s_user_power(用户授权)
| b_user_id | b_power_id | b_canuse | b_updatedatetime |
| --- | ---: | ---: | --- |
| zhangsan | 1001 | 1 | 2026-02-01 09:00:00 |
| zhangsan | 1002 | 1 | 2026-02-01 09:00:00 |
| zhangsan | 1005 | 1 | 2026-02-01 09:00:00 |
| zhangsan | 1007 | 1 | 2026-02-01 09:00:00 |
| zhangsan | 1009 | 1 | 2026-02-01 09:00:00 |
| zhangsan | 1006 | 0 | 2026-02-10 14:30:00 |
| zhangsan | 1008 | 0 | 2026-02-10 14:30:00 |
| zhangsan | 1003 | 1 | 2026-02-01 09:00:00 |
| zhangsan | 1010 | 1 | 2026-02-01 09:00:00 |
| lisi | 1001 | 1 | 2026-01-15 10:00:00 |
| lisi | 1002 | 1 | 2026-01-15 10:00:00 |
| lisi | 1005 | 1 | 2026-01-15 10:00:00 |
| lisi | 1006 | 1 | 2026-01-15 10:00:00 |
| lisi | 1007 | 1 | 2026-01-15 10:00:00 |
| lisi | 1008 | 1 | 2026-01-15 10:00:00 |
| lisi | 1009 | 1 | 2026-01-15 10:00:00 |
| lisi | 1003 | 1 | 2026-01-15 10:00:00 |
| lisi | 1004 | 1 | 2026-01-15 10:00:00 |
| lisi | 1010 | 1 | 2026-01-15 10:00:00 |
张三的 `1006` 对应 `module.cw_fee.create`,`1008` 对应 `module.cw_fee.delete`,两条记录都已停用(`b_canuse = 0`),因此张三没有新增和删除权限。模块操作权限没有用户授权记录时默认放行,所以需要显式停用记录表达禁止操作。
### 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,可删全模块数据。