Files
workspace/code/fms/FMS权限设计.md
T
2026-09-08 17:03:59 +08:00

15 KiB
Raw Blame History

FMS 权限设计

本文是权限部分的定稿设计,以《FMS新系统核心表结构设计.md》的表结构为基础,替代《字段权限与用户个性化设计.md》中与本文冲突的表述(差异见第 8 节)。 用户个性化(s_user_field_pref、s_user_query_field_pref)不在本文范围内,后续单独处理。

0. 设计定位

0.1 系统特征(决定设计取向的前提)

特征 现状 对权限设计的影响
部署方式 一客户一数据库 权限点只需在本库内唯一,编码不带租户前缀;不存在跨租户数据串户风险
架构 元数据驱动(s_module / s_field / s_field_view …) 权限点必须自动生成,不能手工维护清单
客户可定制 可加模块、加字段 权限点需要增量同步机制
客户规模 中小货代,多数无专职 IT 模型必须简单到客户管理员能自行配置
用户规模 几十 ~ 几百 不需要 SAP 级别的授权对象模型

0.2 结论:对应用友 / 金蝶的三层模型

认知模型对标用友 / 金蝶(功能权限 / 数据权限 / 字段权限 + 角色),理由不是技术最优,而是客户和实施顾问无需学习成本,且规模匹配。

不采用:

  • SAP 授权对象模型(为跨国集团设计,规模不匹配,交付成本高)
  • Salesforce 四层共享模型(Profile + Permission Set + OWD + Sharing,计算复杂)
  • 通用权限框架 / ABAC / 运行时策略引擎(明确不做)

借鉴两点:

  • 权限点自动生成(Salesforce 思路,利用本系统元数据驱动的优势)
  • 数据范围支持选择多个组织节点(CargoWise 的 Branch 思路,覆盖"只看上海 + 深圳分公司")

1. 权限模型

1.1 四类权限

类别 回答的问题 载体 默认策略
入口 能不能进 menu / page / report 默认拒绝
动作 能不能做 module.<id>.<action> 全部默认拒绝(权限就是权限)
字段 能看/改哪些列 s_user_field_permission 跟随模块默认,只收紧
数据范围 能看哪些行 s_user_data_scope 未配置视为 all

1.2 入口 ≠ 动作

  • 入口是一维的:能不能打开(菜单可见、页面可访问)。勾了"能进费用管理"不代表能改数据。
  • 动作是多维的:对某个模块能做什么。

两者互不继承,符合《核心表结构》"菜单权限不自动授予模块权限"的约定。

1.3 权限就是权限

不区分「内置动作」与「业务动作」。查看 / 新增 / 修改 / 删除 / 导出、审核 / 核销 / 关闭 / 作废等都是同一种动作权限,只有「勾选」与「未勾选」两种状态。

状态 含义
勾选 有 s_user_power 授权记录(b_canuse = 1)
未勾选 无授权记录 = 拒绝

全部动作统一默认拒绝。简化点:

  • 一种存储:只存「授权记录」,没有「禁止记录」
  • 一种默认策略:未勾选 = 拒绝
  • 一种行为:勾选即授权

客户不能新建动作权限点——业务动作需要对应的后端代码,客户建了也无效。客户界面只提供勾选。

1.4 角色(可选层)

采用主角色 + 个人例外两层,不做角色继承角色:

用户 → 主角色(继承,只有一个)+ 个人例外(覆盖角色中的少数项)
  • 新员工入职 = 选一个角色
  • 个别人特殊 = 加几条例外
  • 改角色 = 该角色所有人跟着变(与 SAP / 用友行为一致)
  • 查询只有两层,不是无限递归

角色不是强制项:不引入角色也能用(直接给用户配),但引入后配置量大幅下降。产品内置「货代标准权限包」(操作员 / 客服 / 费用会计 / 财务经理 / 只读查询),实施时导入客户库后微调。


2. 权限点编码规范

类型 编码 示例
菜单入口 menu.<module_id>.access menu.cw_fee.access
页面入口 page.<page_id>.access page.sea_detail.access
报表入口 report.<report_id>.access report.cw_fee_summary.access
内置动作 module.<module_id>.<read|create|update|delete|export> module.cw_fee.delete
业务动作 module.<module_id>.<action_id> module.cw_fee.audit

规则:

  • 全部小写,点分分隔;<module_id> 用 s_module.b_id
  • 一客户一库,编码在本库内唯一即可,不加租户前缀
  • 权限点由系统生成,不允许手工新增业务动作
  • s_power.b_code 是稳定业务键;s_power.b_id 是内部主键,业务代码只依赖 b_code

与旧模型的差异

项 旧(b_user_power / s_module_power) 本文
权限点标识 ${module_id}.${雪花ID},跨环境 ID 不同,配置无法迁移 语义化 b_code,可跨环境迁移
动作编码 action.module.xx.yy.execute module.xx.yy

现有 stores/permissions.js 依赖旧表(b_user_power / s_module_power),迁移到新模型后需同步改造(见第 6 节)。


3. 权限点的自动生成与升级同步

3.1 生成规则

来源 生成内容
s_module 模块 menu.<id>.access + 5 个内置动作
s_module 中 b_module_type = 'page' 的节点 page.<id>.access(仅"需要独立入口控制"的页面)
报表定义 report.<id>.access
开发注册的业务动作 module.<id>.<action_id>

3.2 增量同步三原则

产品升级或客户新增模块后需要补权限点,同步逻辑必须幂等:

  1. 只增不删 —— 不删除已有权限点(删了会丢客户配置)
  2. 不覆盖已有配置 —— 客户主动停用的 b_canuse = 0 不被重置
  3. 废弃置 b_canuse = 0 —— 不物理删除,保留历史可追溯

3.3 客户自定义字段的权限

客户新增字段(如"拖车费")默认跟随模块默认配置,即全员可见可编辑。

防泄漏建议(低优先级,可后置):

  • 新增字段时,字段名命中 cost / price / amount / profit / 成本 / 价 等关键词 → 提示是否标记为敏感字段
  • 提供「敏感字段体检」:列出金额类型但未标记敏感的字段供确认

4. 权限计算顺序

一次页面访问或接口调用按顺序计算:

  1. 用户账号与对应资源是否启用
  2. 入口:菜单 / 页面 / 报表是否有有效授权(默认拒绝)
  3. 动作:当前动作是否被允许
    • 内置动作:无记录即放行;b_canuse = 0 明确禁止
    • 业务动作:必须有有效授权记录
  4. 字段:按模块字段元数据 + 用户字段策略裁剪(只收紧)
  5. 数据范围:override → default → all,同层多条 OR 合并,生成 SQL 行条件
  6. 角色层:用户例外覆盖角色配置

页面显示与接口执行必须用同一套规则。前端可提前隐藏,但最终以后端 SQL 为准(《开发规范》第 10、23 条)。


5. 后端落实

5.1 统一拦截点

通用数据接口是唯一的 SQL 出入口,权限在此单点落实:

接口 对应动作 需落实
/data/loaddata read 动作校验 + 数据范围 WHERE + 字段裁剪
/data/page read 同上
/data/saveobjt create / update / delete 动作校验 + 字段 edit 校验 + 目标行必须落在数据范围内
/data/loaddatabysql — ⚠️ 见 5.2
/data/nextid /nextcode /describe — 无需

流程:

解析 table → 用 s_module.b_viewtable / b_savetable 反查 module_id
           → 确定 action(loaddata/page → read;saveobjt 按 inserts/updates/deletes 分别定)
           → ① 动作校验
           → ② 数据范围拼入 WHERE
           → ③ SELECT 列剔除 hidden 字段
           → 执行

权限结果按 (user_id, module_id, action) 缓存,避免每次查询都查权限表。

5.2 ⚠️ /data/loaddatabysql 必须收紧

该接口接受任意 SQL,会完全绕过字段裁剪和数据范围。不处理则第 4 节所有规则形同虚设。

处理方案(择一,建议第一种):

  1. 仅限管理员 / 内部账号可用(推荐)
  2. 改为只接受白名单视图名,不接受 SQL 片段
  3. 解析 SQL 后注入条件(复杂且易出漏洞,不推荐)

这与《开发规范》第 8 条「动态配置不等于任意 SQL」一致。

5.3 表名 → 模块 反查

loaddata / page 的参数只带表名,需经 s_module.b_viewtable / b_savetable 反查 module_id。

待确认:是否存在多个模块共用同一视图?若存在,表名 → 模块的映射不唯一,需要前端额外传 moduleId 参数。


6. 前端落实

6.1 stores/permissions.js 改造

现有骨架(并发加载 + 主体去重 + 管理员提权)保留,替换数据源与判断逻辑:

现状 改造后
拉 s_module / b_user_module / b_user_power / s_module_power 拉 s_module / s_power / s_user_power / s_user_field_permission / s_user_data_scope
powerSet 存 ${module_id}.${雪花ID} 存语义化 b_code
canPower(scopeCode, powerCode)(零调用) can(code) 直接传 b_code

对外暴露:

can('module.cw_fee.delete')               // 动作判断
fieldPerm(moduleId, field)                // → { mode, query, export }
scope(moduleId, action)                   // 数据范围

6.2 三个接线点

  1. 按钮级:提供 v-permission 指令 + usePermission(),替换掉零调用的 canPower
  2. 字段裁剪:FmsModuleTable.buildColumns() 之后过滤 —— hidden 移除列、view 置只读、export = 0 剔除导出列
  3. 菜单过滤:stores/menus.js 目前只按 g3soft 管理员裁剪,未使用授权数据,需改为按入口权限过滤

前端裁剪只是展示层,不可信。真正的过滤在后端 SQL。


7. 配置界面设计

7.1 核心原则

  • IT 定义"能分配什么",客户决定"分给谁"。IT 不碰人,客户不碰元数据。
  • 一个页面配完,不分区切换、不重复选模块。
  • 默认展示已配置项,而非全量清单(规则是"没配 = 放行",全量铺开只会增加噪音)。

7.2 树的主体是模块树,不是菜单树

s_module 本身是树(b_parent_id + b_module_type),page 类型节点天然承载"页面包含哪些模块":

海运详细页(page 节点,只配入口)
├── 海运主表(module)
├── 装箱主表(module)
└── 费用管理(module)

菜单 s_menu 只是指向模块的指针(s_menu.b_module_id)。不用菜单树做权限骨架:

  • 菜单不拥有字段 / 动作 / 数据范围,挂上去语义错误
  • 一个模块可能被多个菜单引用 → 同一模块重复出现、配置冲突
  • 与《核心表结构》"权限以业务模块为核心"矛盾

菜单入口权限 = 模块树上的「入口」列。只有"一个模块被多个菜单引用且需分别控制"时,才启用 page.<id>.access 独立入口。

7.3 主界面形态(一屏配完一个人)

模块树            入口 │ 动作(默认拒绝,勾选即授权)   │ 数据范围
──────────────────────────────────────────────────────────────────────────────
▾ 海运详细页[page]  ☑  │      -    -    -    -    -    │        -          │    -
  ▾ 海运主表        ☑  │      ☑    ☑    ☐    ☐    ☐    │  ☑审核 ☐关闭       │ 仅本人▾
    └ 字段(敏感)      金额=只读 · 内部成本=隐藏
  ▾ 装箱主表        ☑  │      ☑    ☐    ☐    ☐    ☐    │        -          │ 本部门▾
  ▾ 费用管理        ☑  │      ☑    ☑    ☑    ☐    ☑    │  ☑审核 ☐反审核      │ 本部门▾
    └ 字段(敏感)      金额=只读 · 内部成本=隐藏 · 毛利=隐藏
▾ 发票管理          ☑  │      ☑    ☑    ☑    ☐    ☑    │  ☑作废             │ 本部门▾
  客户档案          ☐  │      ☑    ☐    ☐    ☐    ☐    │        -          │ 本部门▾

要点:

  • 第一列是模块树,缩进 + 展开 + 父子半选
  • page 节点只配入口,权限列置灰(不直接对应数据表)
  • 动作全在一列,全部默认拒绝,勾选即授权;业务动作与基础操作 chip 同样式
  • 字段是模块行的展开子行,且只显示敏感字段 + 字段组,默认折叠
  • 数据范围是一列,跟着模块走("仅本人"与"禁止删除"是同频操作,不该分开放)

7.4 批量

  • 左侧用户列表支持多选 → 底部出现「应用角色模板」「批量设置数据范围」「清空」
  • 表格支持整列操作(如"所有模块禁止删除")

7.5 IT 专属入口(客户不可见)

内容 频率
模块 / 字段元数据 + 敏感标记 低
字段分组(s_field_group) 低
业务动作注册(需写代码) 低
权限点清单 + 重新生成 低
标准权限包维护 低

7.6 权限诊断(必做)

  • 以某用户身份预览
  • 反查:选中一个权限点 / 字段 → 列出谁有 → 批量调整
  • 查看生效 SQL:排查"为什么这个人看不到这条数据"

8. 与既有文档的差异(决策变更)

# 原表述 本文 原因
1 动作编码 action.module.<module>.<action>.execute module.<module>.<action> CRUD 与业务动作本质是同一概念,统一编码
2 权限直接授予用户,不引入角色继承 可选「主角色 + 个人例外」两层 无角色时配置量是 人 × 模块 × 字段 的笛卡尔积
3 权限点类型含 page / report,与 module 并列 入口与动作分离;动作统一挂模块 入口是"能不能进",动作是"能不能做"
4 数据范围 custom 填单个值 支持选择多个组织节点 "只看上海 + 深圳分公司"
5 未说明树的主语 模块树是主体,菜单只是指针 权限以模块为核心
6 未涉及 权限点自动生成 + 增量同步三原则 一客户一库 + 客户可定制
7 动作分内置 / 业务两种,默认策略不同 不区分,统一为「动作」,全部默认拒绝 简化模型:只存授权记录,没有禁止记录

建议:将第 1、2 条同步修订到《FMS新系统核心表结构设计.md》第 276 行及权限章节,避免两份文档冲突。


9. 实施顺序

阶段 内容 产出
P1 建 s_power / s_user_power(统一编码)+ 权限点自动生成 + /data/* 拦截只做动作校验 能控制"谁能进哪个模块、能做什么"
P2 字段裁剪 + 数据范围 WHERE;收紧 /data/loaddatabysql 后端强制完整
P3 前端 permissions.js 改造 + v-permission + 菜单过滤 + 表格字段裁剪 界面生效
P4 配置界面(模块树 + 一屏配置)+ 标准权限包 + 权限诊断 好用

P1 与 P2 是同一个拦截层的两批规则,不会返工。不建议先做 P4——模型未定,界面做了会返工。