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

338 lines
15 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 权限设计
> 本文是权限部分的**定稿设计**,以《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` |
对外暴露:
```js
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**——模型未定,界面做了会返工。