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

878 lines
23 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 权限体系设计文档
## 1. 设计目标
本权限体系用于 FMS / ERP 元数据驱动架构,核心目标:
1. 操作权限与数据权限分离。
2. 权限直接绑定菜单、动作、模块、字段、页面、报表等资源。
3. Power ID 不要求人工维护,直接由资源类型、资源 ID 和操作组成。
4. 用户直接授权,不引入角色继承。
5. 数据权限独立于菜单,不因为用户从哪个菜单进入而改变数据范围。
6. 用户个性化设置不能突破权限。
7. 权限默认拒绝(Default Deny)。
---
## 2. 权限模型总览
整个权限体系分为两条线:
```text
┌──────────────┐
│ 资源定义 │
└──────┬───────┘
│
┌──────────┼──────────┐
│ │ │
菜单/动作 模块/字段 页面/报表
│ │ │
└──────────┼──────────┘
↓
s_power
↓
s_user_power
↓
用户操作权限
┌──────────────┐
│ 数据模块 │
└──────┬───────┘
↓
s_data_scope
↓
s_user_data_scope
↓
数据范围
```
因此:
- `s_power`:定义「能做什么」
- `s_user_power`:定义「用户能做什么」
- `s_data_scope`:定义「能看到 / 操作哪些数据」
- `s_user_data_scope`:定义「用户在某个模块、某个操作下使用哪个数据范围」
---
## 3. Power ID 设计
### 3.1 核心原则
`b_id` 不再使用随机 ID,也不要求人工创建类似:
```text
power_cw_fee_update
power_cw_fee_audit
power_cw_fee_amount_edit
```
而是直接使用稳定、可解析的权限编码。
统一格式:
```text
menu.<MenuID>
action.<ActionID>
module.<ModuleID>.<Operation>
field.<ModuleID>.<Field>.<Operation>
page.<PageID>
report.<ReportID>
```
### 3.2 Power ID 示例
**菜单权限**
```text
menu.cw_fee
```
表示:可以进入 / 使用费用管理菜单。
**动作权限**
```text
action.cw_fee_audit
```
表示:可以执行费用审核动作。
**模块权限**
```text
module.cw_fee.read
module.cw_fee.create
module.cw_fee.update
module.cw_fee.delete
module.cw_fee.export
```
**字段权限**
```text
field.cw_fee.mx_amount.view
field.cw_fee.mx_amount.edit
field.cw_fee.mx_amount.query
field.cw_fee.mx_amount.export
```
**页面权限**
```text
page.cw_fee
```
**报表权限**
```text
report.cw_fee_summary
```
---
## 4. Power 类型
| b_power_type | 用途 | ID 格式 |
| ------------ | ------------ | --------------------------------------- |
| menu | 菜单入口 | `menu.<MenuID>` |
| action | 业务动作 | `action.<ActionID>` |
| module | 数据模块操作 | `module.<ModuleID>.<Operation>` |
| field | 字段能力 | `field.<ModuleID>.<Field>.<Operation>` |
| page | 页面入口 | `page.<PageID>` |
| report | 报表入口 | `report.<ReportID>` |
---
## 5. 操作权限语义
### 5.1 menu
```text
menu.cw_fee
```
用于判断用户是否拥有菜单入口权限。
菜单权限只解决:
- 用户能不能进入这个菜单。
不负责:
- 数据范围
- 字段权限
- 审核权限
- 删除权限
### 5.2 action
例如:
```text
action.cw_fee_audit
action.cw_fee_unaudit
action.cw_fee_writeoff
```
统一使用:
- `b_power_type = action`
- `b_operation = execute`
动作权限解决:用户能不能执行这个具体业务动作。
### 5.3 module
例如:
```text
module.cw_fee.read
module.cw_fee.create
module.cw_fee.update
module.cw_fee.delete
module.cw_fee.export
```
模块权限解决:用户对该业务模块具有什么数据操作能力。
### 5.4 field
例如:
```text
field.cw_fee.mx_amount.view
field.cw_fee.mx_amount.edit
field.cw_fee.mx_amount.query
field.cw_fee.mx_amount.export
```
字段权限解决:用户对某个字段具有什么能力。
需要注意:`field.xxx.edit` 只是权限允许编辑,并不代表字段最终一定可编辑。最终还要同时考虑:
- 字段权限
- `s_field_edit.b_readonly`
- `s_field_edit.b_disabled`
- 业务状态
- 业务规则
### 5.5 page
例如:
```text
page.cw_fee
```
用于页面访问控制。
页面权限和菜单权限可以同时存在:
```text
menu.cw_fee
page.cw_fee
```
菜单解决入口展示 / 进入菜单,页面解决具体页面访问。
### 5.6 report
例如:
```text
report.cw_fee_summary
```
用于报表访问权限。
报表权限本身不等于数据权限。例如用户拥有 `report.cw_fee_summary`,还需要根据报表对应的数据模块执行数据范围控制。
---
## 6. 核心表结构
以下表结构暂时不加入索引、唯一约束、外键约束,后续根据实际运行情况再补充。
### 6.1 s_power
权限定义表。
```sql
create table dbo.s_power (
b_id varchar(300) not null,
b_name nvarchar(200) not null,
b_i18n varchar(150) null,
b_power_type varchar(20) not null,
-- menu / action / module / field / page / report
b_resource varchar(128) null,
-- action / page / report 等资源 ID
b_menu_id varchar(50) null,
-- 所属菜单,主要用于 menu / action
b_module_id varchar(50) null,
-- 所属业务模块
b_field varchar(50) null,
-- field 权限对应字段
b_operation varchar(30) not null,
-- menu/page/report: access
-- action: execute
-- module: read/create/update/delete/export
-- field: view/edit/query/export
b_canuse tinyint not null default 1,
b_xh int not null default 0
);
```
`b_id` 示例:
```text
menu.cw_fee
action.cw_fee_audit
module.cw_fee.read
module.cw_fee.update
field.cw_fee.mx_amount.view
field.cw_fee.mx_amount.edit
page.cw_fee
report.cw_fee_summary
```
这里的 `b_id` 就是 Power 的业务编码。
不再需要 `power_cw_fee_update`、`power_cw_fee_audit` 这种额外编码。
---
## 7. s_user_power
用户权限授权表。
```sql
create table dbo.s_user_power (
b_user_id varchar(50) not null,
b_power_id varchar(300) not null,
b_canuse tinyint not null default 1,
b_updatedatetime datetime2 null
);
```
语义:
- 存在有效记录 = 用户拥有权限
- 不存在记录 = 用户没有权限
- `b_canuse = 0` = 当前授权无效
当前设计不增加 allow / deny,即采用:
**默认拒绝,明确授权。**
---
## 8. 数据权限
操作权限与数据权限必须分开。
例如:
```text
module.cw_fee.read
```
只表示:用户具有读取费用数据的能力。
它并不表示:用户可以读取所有费用数据。
数据范围由 `s_data_scope` / `s_user_data_scope` 控制。
---
## 9. s_data_scope
数据范围定义表。
```sql
create table dbo.s_data_scope (
b_id varchar(50) not null,
b_name nvarchar(200) not null,
b_i18n varchar(150) null,
b_module_id varchar(50) not null,
-- 数据权限作用的数据模块
b_scope_type varchar(30) not null,
-- own / department / department_tree / all / custom
b_scope_field varchar(50) null,
-- 业务数据中的归属字段
-- 例如 b_inputuser_id / b_department_id / b_owner_id
b_scope_value varchar(100) null,
-- custom 等场景使用
b_canuse tinyint not null default 1,
b_xh int not null default 0
);
```
---
## 10. 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_id varchar(50) not null,
b_xh int not null default 0,
b_updatedatetime datetime2 null
);
```
多个 Scope 同时存在时:
```text
Scope A OR Scope B OR Scope C
```
例如:
```text
张三
cw_fee
read
├── department
└── own
```
表示张三读取费用数据时:department 范围 OR own 范围。
---
## 11. 完整示例
假设系统有:
- 模块:`cw_fee` = 费用管理
- 菜单:`cw_fee` = 费用管理
- 动作:`cw_fee_audit` = 审核、`cw_fee_unaudit` = 反审核、`cw_fee_writeoff` = 核销
- 字段:`mx_amount` = 金额
### 11.1 自动产生 Power
菜单:
```text
menu.cw_fee
```
动作:
```text
action.cw_fee_audit
action.cw_fee_unaudit
action.cw_fee_writeoff
```
模块:
```text
module.cw_fee.read
module.cw_fee.create
module.cw_fee.update
module.cw_fee.delete
module.cw_fee.export
```
字段:
```text
field.cw_fee.mx_amount.view
field.cw_fee.mx_amount.edit
field.cw_fee.mx_amount.query
field.cw_fee.mx_amount.export
```
---
## 12. 用户授权示例
张三拥有:
- `menu.cw_fee`
- `module.cw_fee.read`
- `module.cw_fee.update`
- `action.cw_fee_audit`
- `field.cw_fee.mx_amount.view`
```sql
insert into dbo.s_user_power (
b_user_id,
b_power_id,
b_canuse
)
values
('zhangsan', 'menu.cw_fee', 1),
('zhangsan', 'module.cw_fee.read', 1),
('zhangsan', 'module.cw_fee.update', 1),
('zhangsan', 'action.cw_fee_audit', 1),
('zhangsan', 'field.cw_fee.mx_amount.view', 1);
```
张三没有 `module.cw_fee.delete`、`field.cw_fee.mx_amount.edit`,所以:
- 不能删除费用
- 不能修改金额字段
---
## 13. 数据范围示例
定义:
```sql
insert into dbo.s_data_scope (
b_id,
b_name,
b_module_id,
b_scope_type,
b_scope_field,
b_canuse,
b_xh
)
values
(
'scope.cw_fee.department',
N'本部门',
'cw_fee',
'department',
'b_department_id',
1,
0
);
```
然后授权:
```sql
insert into dbo.s_user_data_scope (
b_user_id,
b_module_id,
b_operation,
b_scope_id,
b_xh
)
values (
'zhangsan',
'cw_fee',
'read',
'scope.cw_fee.department',
0
);
```
此时张三拥有 `module.cw_fee.read`,并且 read 数据范围 = 本部门。
最终不是「张三可以读取全部费用」,而是「张三可以读取自己有权限范围内的费用」。
---
## 14. 权限执行顺序
一个业务操作建议按照以下顺序判断:
```text
① 用户是否有效
↓
② 菜单 / 页面入口权限
↓
③ Action 权限
↓
④ Module 数据操作权限
↓
⑤ Field 字段权限
↓
⑥ Data Scope 数据范围
↓
⑦ 业务状态 / 业务规则
↓
⑧ 执行操作
```
例如「审核费用」:
```text
用户
↓
menu.cw_fee
↓
page.cw_fee
↓
action.cw_fee_audit
↓
module.cw_fee.update
↓
数据范围
↓
费用当前状态 = 送审
↓
执行审核
```
---
## 15. 不同权限解决不同问题
| 问题 | 权限 |
| -------------- | ------------------------------- |
| 能不能看到菜单 | `menu.*` |
| 能不能进入页面 | `page.*` |
| 能不能执行审核 | `action.*` |
| 能不能修改费用 | `module.cw_fee.update` |
| 能不能看到金额 | `field.cw_fee.mx_amount.view` |
| 能不能修改金额 | `field.cw_fee.mx_amount.edit` |
| 能不能查询金额 | `field.cw_fee.mx_amount.query` |
| 能不能导出金额 | `field.cw_fee.mx_amount.export` |
| 能不能看报表 | `report.*` |
| 能看哪些数据 | `s_data_scope` |
---
## 16. 与元数据体系的关系
Power 不应该成为另一套独立的资源体系,而应该从现有元数据自动生成。
```text
s_menu
↓
menu.<MenuID>
Action 定义
↓
action.<ActionID>
s_module
↓
module.<ModuleID>.<Operation>
s_field
↓
field.<ModuleID>.<Field>.<Operation>
Page 定义
↓
page.<PageID>
Report 定义
↓
report.<ReportID>
```
因此新增模块 `cw_air_fee`,系统可以自动注册:
```text
module.cw_air_fee.read
module.cw_air_fee.create
module.cw_air_fee.update
module.cw_air_fee.delete
module.cw_air_fee.export
```
新增字段 `mx_tax`,可以自动注册:
```text
field.cw_air_fee.mx_tax.view
field.cw_air_fee.mx_tax.edit
field.cw_air_fee.mx_tax.query
field.cw_air_fee.mx_tax.export
```
不需要开发人员另外维护 Power 编码。
---
## 17. 权限与用户个性化的关系
用户个性化配置(`s_user_field_pref`、`s_user_query_field_pref`)只能调整:
- 显示
- 排序
- 宽度
- 查询字段默认状态
不能增加权限。
最终优先级:
```text
字段 b_canuse
↓
权限
↓
系统默认布局
↓
用户个性化
```
例如:用户没有 `field.cw_fee.mx_amount.view`,即使 `s_user_field_pref.b_visible = 1`,也不能显示金额字段。
---
## 18. 最终推荐的核心关系
```text
┌───────────────┐
│ s_module │
└───────┬───────┘
│
├──────────────┐
↓ ↓
s_field s_data_scope
│ │
↓ ↓
s_power s_user_data_scope
│
↓
s_user_power
│
↓
b_user
```
菜单和动作:
```text
s_menu
│
├── menu.<MenuID>
│
└── action.<ActionID>
↓
s_power
```
页面和报表:
```text
page / report
↓
s_power
```
---
## 19. 最终结论
本设计的核心原则可以归纳为:
- 资源 ID = 资源本身的 ID
- Power ID = 资源类型 + 资源 ID + Operation
统一规则:
```text
menu.<MenuID>
action.<ActionID>
module.<ModuleID>.<Operation>
field.<ModuleID>.<Field>.<Operation>
page.<PageID>
report.<ReportID>
```
其中:
- `b_id` 是稳定的业务权限编码,不需要人工随机生成。
- `s_power` 是统一权限注册表。
- `s_user_power` 是用户操作权限。
- `s_data_scope` 是数据范围定义。
- `s_user_data_scope` 是用户数据范围授权。
- 操作权限和数据权限完全分离。
- 菜单不负责数据范围。
- 用户个性化不能突破权限。
- 当前不引入角色继承。
- 后续如果增加角色,可以在 `s_user_power` 之外增加角色授权层,而无需改变 Power 编码体系。
---
## 20. Demo 数据(4 张表)
以下为 4 张表的示例数据,沿用正文的 `cw_fee` 费用模块。示例用户:张三(费用会计)、李四(财务经理)。
### 20.1 s_power(权限定义)
| b_id | b_name | b_power_type | b_resource | b_menu_id | b_module_id | b_field | b_operation | b_canuse | b_xh |
| --------------------------------- | ---------- | ------------ | --------------- | --------- | ----------- | ---------- | ----------- | -------- | ---- |
| menu.cw_fee | 费用管理菜单 | menu | cw_fee | cw_fee | null | null | access | 1 | 10 |
| page.cw_fee | 费用页面 | page | cw_fee | cw_fee | null | null | access | 1 | 20 |
| action.cw_fee_audit | 费用审核 | action | cw_fee_audit | cw_fee | cw_fee | null | execute | 1 | 30 |
| action.cw_fee_unaudit | 费用反审核 | action | cw_fee_unaudit | cw_fee | cw_fee | null | execute | 1 | 40 |
| module.cw_fee.read | 费用读取 | module | null | cw_fee | cw_fee | null | read | 1 | 10 |
| module.cw_fee.create | 费用新增 | module | null | cw_fee | cw_fee | null | create | 1 | 20 |
| module.cw_fee.update | 费用修改 | module | null | cw_fee | cw_fee | null | update | 1 | 30 |
| module.cw_fee.delete | 费用删除 | module | null | cw_fee | cw_fee | null | delete | 1 | 40 |
| module.cw_fee.export | 费用导出 | module | null | cw_fee | cw_fee | null | export | 1 | 50 |
| field.cw_fee.mx_amount.view | 金额可查看 | field | null | cw_fee | cw_fee | mx_amount | view | 1 | 10 |
| field.cw_fee.mx_amount.edit | 金额可编辑 | field | null | cw_fee | cw_fee | mx_amount | edit | 1 | 20 |
| field.cw_fee.mx_amount.query | 金额可查询 | field | null | cw_fee | cw_fee | mx_amount | query | 1 | 30 |
| field.cw_fee.mx_amount.export | 金额可导出 | field | null | cw_fee | cw_fee | mx_amount | export | 1 | 40 |
| report.cw_fee_summary | 费用汇总报表 | report | cw_fee_summary | cw_fee | cw_fee | null | access | 1 | 60 |
要点:覆盖全部 6 种 `b_power_type`;`b_id` 由类型 + 资源 + 操作自动拼出,`b_module_id` / `b_field` 等列是解析冗余,便于按模块、字段反查权限。
### 20.2 s_user_power(用户授权)
| b_user_id | b_power_id | b_canuse | b_updatedatetime |
| --------- | ------------------------------ | -------- | -------------------- |
| zhangsan | menu.cw_fee | 1 | 2026-02-01 09:00:00 |
| zhangsan | page.cw_fee | 1 | 2026-02-01 09:00:00 |
| zhangsan | module.cw_fee.read | 1 | 2026-02-01 09:00:00 |
| zhangsan | module.cw_fee.update | 1 | 2026-02-01 09:00:00 |
| zhangsan | module.cw_fee.export | 1 | 2026-02-01 09:00:00 |
| zhangsan | module.cw_fee.delete | **0** | 2026-02-10 14:30:00 |
| zhangsan | action.cw_fee_audit | 1 | 2026-02-01 09:00:00 |
| zhangsan | field.cw_fee.mx_amount.view | 1 | 2026-02-01 09:00:00 |
| zhangsan | field.cw_fee.mx_amount.query | 1 | 2026-02-01 09:00:00 |
| zhangsan | report.cw_fee_summary | 1 | 2026-02-01 09:00:00 |
| lisi | menu.cw_fee | 1 | 2026-01-15 10:00:00 |
| lisi | page.cw_fee | 1 | 2026-01-15 10:00:00 |
| lisi | module.cw_fee.read | 1 | 2026-01-15 10:00:00 |
| lisi | module.cw_fee.create | 1 | 2026-01-15 10:00:00 |
| lisi | module.cw_fee.update | 1 | 2026-01-15 10:00:00 |
| lisi | module.cw_fee.delete | 1 | 2026-01-15 10:00:00 |
| lisi | module.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 |
| lisi | field.cw_fee.mx_amount.view | 1 | 2026-01-15 10:00:00 |
| lisi | field.cw_fee.mx_amount.edit | 1 | 2026-01-15 10:00:00 |
| lisi | field.cw_fee.mx_amount.export | 1 | 2026-01-15 10:00:00 |
要点:
- 张三的 `module.cw_fee.delete` 记录存在但 `b_canuse = 0`,演示「授权被手动停用 = 无权限」,与「无记录 = 无权限」效果相同;
- 张三没有 `field.cw_fee.mx_amount.edit`,金额字段对他只读;李四全量字段能力。
### 20.3 s_data_scope(数据范围定义)
| b_id | b_name | b_module_id | b_scope_type | b_scope_field | b_scope_value | b_canuse | b_xh |
| ------------------------------ | ------------ | ----------- | ---------------- | ---------------- | ------------- | -------- | ---- |
| scope.cw_fee.own | 仅本人 | cw_fee | own | b_inputuser_id | null | 1 | 10 |
| scope.cw_fee.department | 本部门 | cw_fee | department | b_department_id | null | 1 | 20 |
| scope.cw_fee.department_tree | 本部门及下属 | cw_fee | department_tree | b_department_id | null | 1 | 30 |
| scope.cw_fee.all | 全部费用 | cw_fee | all | null | null | 1 | 40 |
| scope.cw_fee.custom_project | 指定项目 | cw_fee | custom | b_project_id | PRJ001 | 1 | 50 |
要点:`own` / `department` / `department_tree` / `all` 靠 `b_scope_type` 语义过滤,`b_scope_field` 指向业务表的归属字段;`custom` 额外用 `b_scope_value` 指定具体值。
### 20.4 s_user_data_scope(用户数据范围授权)
| b_user_id | b_module_id | b_operation | b_scope_id | b_xh | b_updatedatetime |
| --------- | ----------- | ----------- | ---------------------------- | ---- | ------------------- |
| zhangsan | cw_fee | read | scope.cw_fee.department | 0 | 2026-02-01 09:00:00 |
| zhangsan | cw_fee | read | scope.cw_fee.own | 10 | 2026-02-01 09:00:00 |
| zhangsan | cw_fee | update | scope.cw_fee.own | 0 | 2026-02-01 09:00:00 |
| lisi | cw_fee | read | scope.cw_fee.department_tree | 0 | 2026-01-15 10:00:00 |
| lisi | cw_fee | update | scope.cw_fee.department_tree | 0 | 2026-01-15 10:00:00 |
| lisi | cw_fee | delete | scope.cw_fee.all | 0 | 2026-01-15 10:00:00 |
要点:
- 张三 read 挂了两条范围 → 部门数据 OR 本人数据;
- 数据范围按操作分开授权:张三能看本部门,但只能改自己录的费用(update 仅 own);
- 李四 delete 挂 `all`,经理可删全模块数据,符合「操作越重、范围越收」或「管理者放宽」都可表达的弹性。
### 20.5 组合结果解读
| 能力 | 张三(费用会计) | 李四(财务经理) |
| ---------------- | ---------------------------------------- | -------------------------------- |
| 看菜单 / 页面 | ✅ | ✅ |
| 读取费用数据 | ✅ 本部门 + 本人 | ✅ 本部门及下属部门 |
| 新增 | ❌ 无 module.create | ✅ |
| 修改 | ✅ 仅本人录入的单据 | ✅ 本部门及下属部门的单据 |
| 删除 | ❌ 授权已停用(b_canuse=0) | ✅ 全部数据 |
| 审核 / 反审核 | ✅ 可审核,❌ 不可反审核 | ✅ 都可以 |
| 金额字段 | 可看、可查询、❌ 不可编辑、❌ 不可导出字段列 | 可看、可编辑、可导出 |
| 汇总报表 | ✅ | ✅(报表数据仍受 read 范围约束) |