15 KiB
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 增量同步三原则
产品升级或客户新增模块后需要补权限点,同步逻辑必须幂等:
- 只增不删 —— 不删除已有权限点(删了会丢客户配置)
- 不覆盖已有配置 —— 客户主动停用的
b_canuse = 0不被重置 - 废弃置
b_canuse = 0—— 不物理删除,保留历史可追溯
3.3 客户自定义字段的权限
客户新增字段(如"拖车费")默认跟随模块默认配置,即全员可见可编辑。
防泄漏建议(低优先级,可后置):
- 新增字段时,字段名命中
cost / price / amount / profit / 成本 / 价等关键词 → 提示是否标记为敏感字段 - 提供「敏感字段体检」:列出金额类型但未标记敏感的字段供确认
4. 权限计算顺序
一次页面访问或接口调用按顺序计算:
- 用户账号与对应资源是否启用
- 入口:菜单 / 页面 / 报表是否有有效授权(默认拒绝)
- 动作:当前动作是否被允许
- 内置动作:无记录即放行;
b_canuse = 0明确禁止 - 业务动作:必须有有效授权记录
- 内置动作:无记录即放行;
- 字段:按模块字段元数据 + 用户字段策略裁剪(只收紧)
- 数据范围:
override→default→all,同层多条 OR 合并,生成 SQL 行条件 - 角色层:用户例外覆盖角色配置
页面显示与接口执行必须用同一套规则。前端可提前隐藏,但最终以后端 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 节所有规则形同虚设。
处理方案(择一,建议第一种):
- 仅限管理员 / 内部账号可用(推荐)
- 改为只接受白名单视图名,不接受 SQL 片段
- 解析 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 三个接线点
- 按钮级:提供
v-permission指令 +usePermission(),替换掉零调用的canPower - 字段裁剪:
FmsModuleTable.buildColumns()之后过滤 ——hidden移除列、view置只读、export = 0剔除导出列 - 菜单过滤:
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——模型未定,界面做了会返工。