# 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..` | 全部默认拒绝(权限就是权限) | | **字段** | 能看/改哪些列 | `s_user_field_permission` | 跟随模块默认,只收紧 | | **数据范围** | 能看哪些行 | `s_user_data_scope` | 未配置视为 `all` | ### 1.2 入口 ≠ 动作 - **入口**是一维的:能不能打开(菜单可见、页面可访问)。勾了"能进费用管理"**不代表**能改数据。 - **动作**是多维的:对某个模块能做什么。 两者互不继承,符合《核心表结构》"菜单权限不自动授予模块权限"的约定。 ### 1.3 权限就是权限 不区分「内置动作」与「业务动作」。查看 / 新增 / 修改 / 删除 / 导出、审核 / 核销 / 关闭 / 作废等都是同一种**动作权限**,只有「勾选」与「未勾选」两种状态。 | 状态 | 含义 | | --- | --- | | 勾选 | 有 `s_user_power` 授权记录(`b_canuse = 1`) | | 未勾选 | 无授权记录 = 拒绝 | **全部动作统一默认拒绝**。简化点: - 一种存储:只存「授权记录」,没有「禁止记录」 - 一种默认策略:未勾选 = 拒绝 - 一种行为:勾选即授权 > 客户不能新建动作权限点——业务动作需要对应的后端代码,客户建了也无效。客户界面只提供勾选。 ### 1.4 角色(可选层) 采用**主角色 + 个人例外**两层,不做角色继承角色: ``` 用户 → 主角色(继承,只有一个)+ 个人例外(覆盖角色中的少数项) ``` - 新员工入职 = 选一个角色 - 个别人特殊 = 加几条例外 - 改角色 = 该角色所有人跟着变(与 SAP / 用友行为一致) - 查询只有两层,不是无限递归 角色不是强制项:不引入角色也能用(直接给用户配),但引入后配置量大幅下降。产品内置「货代标准权限包」(操作员 / 客服 / 费用会计 / 财务经理 / 只读查询),实施时导入客户库后微调。 --- ## 2. 权限点编码规范 | 类型 | 编码 | 示例 | | --- | --- | --- | | 菜单入口 | `menu..access` | `menu.cw_fee.access` | | 页面入口 | `page..access` | `page.sea_detail.access` | | 报表入口 | `report..access` | `report.cw_fee_summary.access` | | 内置动作 | `module..` | `module.cw_fee.delete` | | 业务动作 | `module..` | `module.cw_fee.audit` | 规则: - 全部小写,点分分隔;`` 用 `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..access` + 5 个内置动作 | | `s_module` 中 `b_module_type = 'page'` 的节点 | `page..access`(仅"需要独立入口控制"的页面) | | 报表定义 | `report..access` | | 开发注册的业务动作 | `module..` | ### 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..access` 独立入口。 ### 7.3 主界面形态(一屏配完一个人) ``` 模块树 入口 │ 动作(默认拒绝,勾选即授权) │ 数据范围 ────────────────────────────────────────────────────────────────────────────── ▾ 海运详细页[page] ☑ │ - - - - - │ - │ - ▾ 海运主表 ☑ │ ☑ ☑ ☐ ☐ ☐ │ ☑审核 ☐关闭 │ 仅本人▾ └ 字段(敏感) 金额=只读 · 内部成本=隐藏 ▾ 装箱主表 ☑ │ ☑ ☐ ☐ ☐ ☐ │ - │ 本部门▾ ▾ 费用管理 ☑ │ ☑ ☑ ☑ ☐ ☑ │ ☑审核 ☐反审核 │ 本部门▾ └ 字段(敏感) 金额=只读 · 内部成本=隐藏 · 毛利=隐藏 ▾ 发票管理 ☑ │ ☑ ☑ ☑ ☐ ☑ │ ☑作废 │ 本部门▾ 客户档案 ☐ │ ☑ ☐ ☐ ☐ ☐ │ - │ 本部门▾ ``` 要点: - 第一列是**模块树**,缩进 + 展开 + 父子半选 - `page` 节点只配入口,权限列置灰(不直接对应数据表) - 动作全在一列,全部默认拒绝,勾选即授权;业务动作与基础操作 chip 同样式 - **字段是模块行的展开子行**,且只显示**敏感字段 + 字段组**,默认折叠 - 数据范围是一列,跟着模块走("仅本人"与"禁止删除"是同频操作,不该分开放) ### 7.4 批量 - 左侧用户列表支持多选 → 底部出现「应用角色模板」「批量设置数据范围」「清空」 - 表格支持整列操作(如"所有模块禁止删除") ### 7.5 IT 专属入口(客户不可见) | 内容 | 频率 | | --- | --- | | 模块 / 字段元数据 + 敏感标记 | 低 | | 字段分组(`s_field_group`) | 低 | | 业务动作注册(需写代码) | 低 | | 权限点清单 + 重新生成 | 低 | | 标准权限包维护 | 低 | ### 7.6 权限诊断(必做) - **以某用户身份预览** - **反查**:选中一个权限点 / 字段 → 列出谁有 → 批量调整 - **查看生效 SQL**:排查"为什么这个人看不到这条数据" --- ## 8. 与既有文档的差异(决策变更) | # | 原表述 | 本文 | 原因 | | --- | --- | --- | --- | | 1 | 动作编码 `action.module...execute` | `module..` | 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**——模型未定,界面做了会返工。