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

24 KiB
Raw Blame History

FMS 权限体系、字段权限与用户个性化设计

0. 总体约定

本系统是 SQL 驱动的内部 ERP,权限表和模块配置表主要用于生成通用查询、写入和界面配置规则。

三类权限(正交,不要混用「数据权限」这个统称)

类别 回答的问题 存储 示例
操作权限(功能权限) 能不能做这个动作 s_user_power(权限点编码) action.cw_fee.delete — 能不能删除
数据范围 能对哪些行做 s_user_data_scope(拼 SQL 行条件) 只能删本部门的
字段权限 能看到 / 改哪些列 s_user_field_permission 看不到内部成本列

三者是交集关系:create / update / delete / export 属于操作权限(编码为 action.<模块>.<操作>),不是数据范围;数据范围只描述行的可见与可操作边界,即使拥有删除权限也仍受范围限制。

权限编码规范

权限点编码即 s_power.b_id,也是 s_user_power.b_power_id 的外键值。格式为 类型.对象[.能力],用点号分隔层级,全小写:

类型 编码 含义 生成时机
page page.<页面编码> 逻辑页面入口(按需启用) 定义页面时
menu menu.<菜单编码> 菜单入口 保存菜单时自动生成
module module.<模块编码> 模块访问(≈ read,能否查看该模块数据) 模块启用时自动生成
action action.<模块编码>.<操作或动作> 数据操作 create / update / delete / export,以及业务动作 audit / settle 等 模块启用或动作注册时自动生成
report report.<报表编码> 报表入口 定义报表时

约定:

  • 点号分隔层级,从左到右依次为 类型 → 对象 → 能力;
  • 对象编码本身不允许包含 .:模块 / 菜单 / 页面 / 报表编码统一不含点号,否则无法按段解析;
  • _ 只用于编码内部(如 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) 比较。

权限采用“入口默认拒绝、字段与数据范围默认不限制、用户配置负责收紧”的策略(模块操作的默认行为见「待确认事项」第 1 条):

  • menu.*、page.*、report.* 与 action.* 属于入口或动作权限,必须存在有效的用户授权记录;page、report 仅在页面或报表需要独立入口控制时启用,启用后同样必须存在有效授权记录;
  • module.<模块编码> 表示模块访问;模块级数据操作统一编码为 action.<模块编码>.<操作>,与业务动作同级;模块操作没有用户授权记录时的默认行为见「待确认事项」第 1 条;s_power 权限点或用户授权记录存在 b_canuse = 0 时明确禁用;
  • 字段没有有效的用户字段策略时,按模块字段默认配置执行,不额外限制查询、编辑和导出;有效字段策略只能收紧权限;
  • 数据范围没有配置时按 all 处理;有配置时先使用操作级 override,没有 override 再使用模块默认的 * 范围;同一层级多条范围按 OR 合并;
  • 用户个性化只影响列表布局、查询收起态、顺序和宽度,不得越过字段权限、字段启用状态或数据范围;
  • 数据库层不建 check 约束,字段取值及组合规则由业务层校验(见各表下方「业务层约束」)。

权限过滤、字段裁剪和数据范围条件最终必须落实到查询 SQL 中,前端隐藏菜单、按钮和字段只用于界面展示。

当前落地方式(阶段性):后端是通用数据网关,/data/loaddata 直接接受 search_condition(行过滤)与 search_columns(列裁剪),自身不做权限判断。因此本阶段由前端拼装 SQL 条件实现权限过滤:

  • 数据范围 → 拼进 search_condition;
  • 字段 hidden 策略 → 从 search_columns 中剔除;
  • 拼装逻辑收敛在前端唯一的纯函数工具里(不散落在各页面),后续后端具备能力时可原样下沉;
  • 这只是执行位置的前移,不改变任何权限规则;前端拼装属于界面层控制,敏感数据仍应在具备条件后补充后端校验。

资源与权限归属

权限以业务模块为核心,不以菜单或 Vue 路由为核心:

资源 主要职责 是否授予模块数据权限
menu 导航入口、菜单显示 否
page 逻辑页面访问(按需启用) 否
module 模块访问(module.<模块编码>)、字段和数据范围;数据操作以 action.<模块编码>.<操作> 表示 是
action 审核、反审核、核销等具体业务动作,以及 create/update/delete/export 通常绑定业务模块
Vue 路由/组件 技术实现和页面复用 否
  • 菜单或页面权限不会自动授予所使用模块的权限;
  • 一个页面可以使用多个模块,一个模块可以被多个页面复用,模块权限始终按模块自身计算;
  • Vue 路由和组件不是稳定的权限标识。page 权限使用稳定的逻辑页面编码,不使用路由路径或组件名称;
  • s_menu.b_module_id(如果保留)只表示菜单的默认/主模块,不能表示页面使用的全部模块,也不能触发权限继承;
  • 普通菜单页面通常只配置菜单入口权限。只有需要独立直接访问的页面,才额外定义 page.<页面编码> 权限,避免客户重复勾选菜单和页面。

复合页面和公共页面

以海运详细页为例:

海运详细页
├── sea_main
├── container_main
├── container_detail
├── finance_main
└── finance_detail

技术人员只需配置一次“页面使用哪些模块”的关系。页面打开后,各模块分别检查自己的 read、字段权限和数据范围:

  • 必需模块没有读取权限时,页面不能完成正常业务,可以拒绝进入;
  • 非必需模块没有读取权限时,隐藏对应页签或区域;
  • 页面权限、菜单权限和模块权限互不继承;
  • 同一个公共 Vue 页面被多个模块使用时,以当前页面配置和实际 module_id 判断权限,不以路由名称判断权限。

动作权限按业务归属配置:有明确业务主模块时使用 action.<module_id>.<action_id>(b_owner_type = 'module');真正跨模块且没有主模块的页面流程才使用 action.<page_id>.<action_id>(b_owner_type = 'page');不把动作绑定到 Vue 路由或公共组件,也不把动作绑定到菜单。

权限计算顺序

一个页面或接口按以下顺序计算最终权限:

  1. 用户账号和对应资源是否启用;
  2. 菜单入口是否有有效授权;存在独立页面/报表权限时,再检查对应入口授权;
  3. 当前动作是否有有效 action.<模块>.<动作> 授权;
  4. 当前模块操作是否被 s_power 或 s_user_power.b_canuse = 0 明确禁用;未配置授权记录时的默认行为见「待确认事项」第 1 条;
  5. 根据模块字段元数据和用户字段策略裁剪字段、查询和导出能力(当前由前端从 search_columns 中剔除隐藏列);
  6. 根据数据范围的 override、default 或 all 生成 SQL 行条件(当前由前端拼装进 search_condition);
  7. 在以上结果基础上应用用户列表布局和查询布局偏好。

页面显示权限和接口执行权限必须使用同一套计算规则。前端可以提前隐藏无权限内容,但不能代替后端 SQL 的最终判断。

1. 表结构

1.1 s_power(权限定义表)

create table dbo.s_power (
    b_id varchar(100) not null,           -- 权限编码本身即主键:menu.cw_fee / module.cw_fee / action.cw_fee.audit
    b_name nvarchar(200) not null,
    b_i18n varchar(150) null,
    b_power_type varchar(20) not null,    -- page/menu/module/action/report
    b_owner_type varchar(20) not null,    -- 归属对象类型:page/menu/module/report;action 时为所属模块或页面
    b_owner_id varchar(50) not null,      -- 归属对象编码:菜单/模块/页面/报表编码
    b_capability varchar(30) null,        -- action 的操作或动作编码:create/update/delete/export/audit...;入口类为 null
    b_operation varchar(30) null,         -- 兼容列:access/execute,由 b_power_type 派生,后续可移除
    b_canuse tinyint not null default 1,
    b_xh int not null default 0,
    constraint PK_s_power primary key (b_id)
);

create index IX_s_power_owner
    on dbo.s_power (b_power_type, b_owner_type, b_owner_id, b_capability, b_canuse);

业务层约束(数据库不建 check 约束,由业务层校验):

  • b_id 由 类型.对象[.能力] 规则生成(见「权限编码规范」),生成后不可编辑,变更时删除重建;
  • b_power_type 只能取 page / menu / module / action / report;
  • page / menu / module / report 类型:b_capability 必须为 null,b_operation 固定为 access;
    • module.<模块编码> 表示模块访问(≈ read);模块的数据操作不在这里定义,统一用 action.<模块编码>.<操作>;
  • action 类型:b_owner_type 只能取 module / page,b_owner_id 必填,b_capability 必填,b_operation 固定为 execute;
  • 动作归属模块、不归属菜单:业务动作统一 b_owner_type = 'module',避免同一个动作在不同菜单下被重复配置;
  • b_id 与 (b_power_type, b_owner_type, b_owner_id, b_capability) 一一对应,新建前先按组合查重。

与现有库(FMS-NEW)的差异:现有 s_power 已采用 b_id varchar(50) 编码主键,并含 b_resource、b_module_id、b_operation 列。迁移时把 b_id 放宽到 varchar(100),补 b_owner_type / b_owner_id / b_capability 三列;b_module_id 与 b_resource 作为兼容列保留,新代码统一读写 b_owner_*。全部元数据表(s_module、s_menu、s_field*)均为字符串业务键主键,权限点保持一致,不引入代理键。

1.2 s_user_power(用户权限授权表)

create table dbo.s_user_power (
    b_user_id varchar(50) not null,
    b_power_id varchar(100) not null,     -- 引用 s_power.b_id,即权限编码:menu.cw_fee / action.cw_fee.audit
    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);

业务层约束:

  • b_power_id 存权限编码而非内部 ID,可直接阅读与按前缀查询,判断时无需 join s_power;
  • 采用「默认拒绝」策略时只保存 b_canuse = 1 的授权记录,没有记录即无权限;
  • 采用「默认放行」策略时,被明确禁止的权限点保存 b_canuse = 0(见「待确认事项」第 1 条);
  • 权限点本身 b_canuse = 0 时,所有用户的该权限一律不生效。

1.3 s_user_field_permission(用户字段访问策略表)

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(用户数据范围授权表)

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' 时允许使用
    -- 值域与 action.<模块编码>.<操作> 的 capability 一致,用于把「操作」和「该操作的行范围」串起来

    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(用户列表布局偏好表)

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(用户查询布局偏好表)

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_name b_power_type b_owner_type b_owner_id b_capability b_operation b_canuse b_xh
menu.cw_fee 费用管理菜单 menu menu cw_fee null access 1 10
page.cw_fee 费用页面 page page cw_fee null access 1 20
module.cw_fee 费用模块访问 module module cw_fee null 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
report.cw_fee_summary 费用汇总报表 report report cw_fee_summary null access 1 100

2.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 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
zhangsan report.cw_fee_summary 1 2026-02-01 09:00:00
zhangsan action.cw_fee.create 0 2026-02-10 14:30:00
zhangsan action.cw_fee.delete 0 2026-02-10 14:30: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 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
lisi report.cw_fee_summary 1 2026-01-15 10:00:00

张三没有 action.cw_fee.create / action.cw_fee.delete 权限:

  • 默认放行策略下,模块操作没有授权记录即视为允许,因此必须显式保存 b_canuse = 0 来禁止(表中后两条);
  • 默认拒绝策略下则不需要这两行,没有记录就是没有权限(见「待确认事项」第 1 条)。

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. 权限在哪里配置

权限分「定义端」和「授权端」,两者分离:

位置 配置内容 写入的表
模块管理 字段敏感标记(只有敏感字段才进入授权界面);自动生成 module.<模块编码> 与 action.<模块编码>.<操作或动作>(数据操作由模块元数据生成,业务动作由动作注册生成) s_module、s_field、s_power
菜单管理 只管入口:menu.<菜单编码> 随菜单保存自动生成;b_module_id 仅表示主模块,不继承任何权限;不再在菜单上手工录入 action 权限点 s_menu、s_power
用户授权(授权端) 选用户 → 勾入口 / 动作 → 设字段策略 → 设数据范围;支持角色模板批量套用 s_user_power、s_user_field_permission、s_user_data_scope
个人偏好 列可见 / 顺序 / 宽度、查询收起态字段 s_user_field_pref、s_user_query_field_pref

规则:

  • 动作归属模块而非菜单,同一个动作只配置一次,不随菜单重复;
  • 勾了菜单不等于拥有模块权限,反之亦然,两者分别授权;
  • 授权界面可为操作便利做联动(勾菜单时自动带出其 b_module_id 指向的模块),但落库仍是两条独立记录;
  • 菜单 b_canuse = 0 时一票否决,所有用户均不可见。

现状提示:当前系统只有菜单管理维护权限点字典,s_user_power 等授权表尚未建立,也没有授权界面;权限过滤目前只有登录态校验,其余全部未落地。

4. 待确认事项

  1. 默认拒绝 还是 默认放行(未拍板):入口类(menu / page / report)一律默认拒绝;action.<模块>.<操作> 在没有授权记录时——默认拒绝(没有记录即无权限,更安全,配合角色模板降低配置量,推荐)还是默认放行(只存 b_canuse = 0 的例外,配置量小但新增动作会自动对所有人生效)。本文示例按两种都做了说明,定稿后需统一。
  2. 角色:s_role / s_user_role / s_role_power 等表已在 200_rebuild_business_keys.sql 中删除。是只做「用户级授权 + 角色模板批量展开」,还是重建角色表。
  3. action 归属修正:现有实现把 action 权限点挂在菜单上(s_power.b_module_id 存的是菜单 id),需改为挂模块;菜单未绑定 b_module_id 时的兜底规则待定。
  4. 数据范围的数据来源:登录态目前只有 userId,没有部门 / 部门树信息,own / department / department_tree 条件的取值来源待定(需要在登录返回或用户信息中补充)。
  5. 后端校验:当前阶段权限过滤由前端拼 SQL 实现,后端仅校验登录态与 SQL 只读性。敏感数据的最终防线仍需后端校验,待后端具备能力后下沉。