Files
workspace/code/fms/FMS模块关系与权限边界重构计划.md
T
2026-09-19 21:17:02 +08:00

548 lines
23 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 模块关系与权限边界重构计划
> 状态:草案,供评审
>
> 目标:明确模块树、菜单绑定、数据关系、权限和删除策略的边界,降低“模块关联”配置的理解成本,避免一个关系表同时承担多种语义。
## 一、结论先行
“模块关联”不应该同时承担菜单授权、模块父子关系、业务数据关系和删除规则。
本次计划采用以下分工:
| 问题 | 权威数据 | 说明 |
| --- | --- | --- |
| 模块业务组织层级 | `s_module.b_parent_id` | 表达业务域、模块分组和权限上下文边界 |
| 菜单导航层级 | `s_menu.b_parent_id` | 表达目录、页面和外链的导航树 |
| 菜单使用哪些模块 | `s_menu_module` | 菜单与模块的多对多绑定,保存明确的模块清单 |
| 用户拥有哪些权限 | `s_power`、`s_user_power` | 模块访问、业务动作和菜单入口权限 |
| 数据字段之间的引用关系 | `s_relation` | 只表达模块字段之间的结构关系 |
| 删除前的业务限制 | `s_delete_rule` | 用只读 SQL 检查状态、单据和跨表条件 |
| 关联记录如何处置 | 删除策略 | `restrict`、`cascade`、`none`,不参与菜单权限计算 |
因此:
1. **菜单权限不需要依赖 `s_relation`。**
2. **模块父子关系使用 `s_module.b_parent_id`,不从字段关系推导。**
3. **`s_relation` 建议保留为“数据关系”,但不再称为菜单权限意义上的模块关联。**
4. **删除策略继续存在,但从数据关系的菜单中剥离,统一放到删除策略入口。**
当前设计文档已经有这几张表的雏形:模块树使用 `s_module.b_parent_id`,菜单绑定使用 `s_menu_module`,数据关系使用 `s_relation`。[核心表设计](FMS新系统核心表结构设计.md) [模块设计评审](FMS模块设计评审.md)
## 二、当前问题
### 2.1 “模块关联”包含了三种不同关系
目前容易把下面三件事都叫作模块关联:
1. **组织关系**:海运模块下面有主单、箱、费用模块。
2. **数据关系**:箱表的 `order_id` 引用主单表的 `id`。
3. **授权关系**:某个菜单页面使用主单模块和箱模块,用户可以进入并操作这些模块。
它们的数学结构和运行规则不同:
| 关系 | 结构 | 典型约束 |
| --- | --- | --- |
| 模块组织 | 树 | 一个父节点、不能循环 |
| 数据关系 | 图 | 可以多对多、可以有多个入边 |
| 菜单绑定 | 多对多 | 一个页面可用多个模块,一个模块可被多个页面使用 |
| 权限授权 | 用户到权限点 | 不能因为数据引用就自动获得权限 |
如果用数据关系建立模块父子树,会遇到多个父节点、循环和跨业务域引用。例如“费用 → 客户”是数据引用,但费用模块并不是客户模块的子模块。把它用于权限继承会造成权限扩大。
### 2.2 删除策略和菜单权限没有共同语义
删除策略回答:
> 删除一条业务记录时,关联记录如何处理?什么条件下不允许删除?
权限回答:
> 用户能否进入页面、使用模块、执行动作、查看字段和查看哪些数据?
两者的计算入口、管理员和风险都不同。把删除配置放进菜单权限关联,会让配置人员误以为“菜单绑定”会影响删除,也会让关系面板出现过多不相关列。
### 2.3 当前代码已经有两个真实消费者
当前前端菜单管理使用 `s_menu_module` 保存菜单与模块的绑定;模块关系面板维护 `s_relation`。后端 `DataDeleteService` 会读取启用的 `s_relation`,展开 `cascade` 并检查 `restrict`。
这说明 `s_relation` 不是完全没有价值,但它的价值来自**数据删除和数据影响分析**,不是菜单授权。若以后改成每个模块完全由专用 SQL 删除,才可以进一步评估是否移除它。
## 三、目标模型
### 3.1 三棵树、两类关系
```text
模块组织树
s_module.b_parent_id
海运
├── 海运主单
├── 箱信息
└── 费用
菜单导航树
s_menu.b_parent_id
业务管理
└── 海运工作台
菜单与模块绑定
s_menu_module
海运工作台 ── 海运主单
海运工作台 ── 箱信息
海运工作台 ── 费用
数据字段关系
s_relation
箱信息.order_id ──> 海运主单.id
费用.order_id ──> 海运主单.id
```
模块树只表达业务组织。菜单树只表达导航。菜单页面使用哪些数据模块由 `s_menu_module` 明确保存。数据字段关系只用于数据层,不参与菜单树和权限树。
### 3.2 权限归属:模块为主,菜单为入口
权限应当**跟着模块**,菜单只负责提供入口。
```text
用户
└── 拥有权限点
├── menu.* → 能否进入菜单页面
├── module.* → 能否使用模块
└── action.* → 能否执行模块动作
菜单页面
└── s_menu_module → 页面使用哪些模块
模块
├── s_user_field_power → 字段能力
└── s_user_data_power → 数据范围
```
菜单绑定模块不等于授予模块权限。原因有三点:
1. 一个模块可能被多个菜单复用,权限应该保持一致;
2. 一个菜单可能使用主模块、明细模块和多个辅助模块,不适合把它们的权限合并成一个菜单权限;
3. 后端接口、导出、保存和业务动作可能绕过菜单入口,必须仍按模块权限和动作权限校验。
因此运行时按以下顺序判断:
1. 用户是否拥有当前菜单的 `menu.*` 权限;
2. 当前页面所需模块是否拥有 `module.*` 权限;
3. 当前请求的操作是否拥有对应的 `action.*` 或标准操作权限;
4. 查询和写入时继续追加字段权限和数据范围条件。
如果一个页面绑定多个模块,菜单权限只决定能否进入页面,每个模块仍单独判断。主模块或必需模块无权限时,页面应隐藏或拒绝进入;可选模块无权限时,可以隐藏对应面板。是否为“必需模块”属于页面配置,不由 `s_relation` 推导。
模块父子树可以作为管理员批量授权的操作范围,但第一期不建议运行时隐式继承:勾选父模块时可以在界面展开子模块,最终保存每个数据模块的明确授权行。这样新建子模块不会在用户不知情的情况下自动获得所有权限。
### 3.3 权限计算边界
权限计算分为五层:
1. `menu.*`:能否看到和进入菜单页面。
2. `module.*`:能否使用页面中的某个模块。
3. `action.*`:能否执行审核、结算等业务动作。
4. `s_user_field_power`:字段查看、编辑、查询和导出权限。
5. `s_user_data_power`:数据行范围。
菜单绑定多个模块时,不合并这些模块的权限;每个模块仍按自己的模块编码判断。数据关系也不自动传播权限。若管理员在界面上勾选一个模块分类节点,可以自动展开下级数据模块,但落库仍保存明确的模块权限或菜单绑定行。
### 3.4 用户授权页面如何呈现
管理员不需要直接理解 `s_relation`。授权页面以业务域和模块树呈现:
```text
海运(业务域,只做分组)
├── 海运主单(data)
├── 装箱信息(data)
└── 费用(data)
```
建议将授权页面分成三个视图:
#### 菜单入口
左侧显示菜单树,管理员选择“海运工作台”等页面,保存 `menu.*` 权限。
#### 模块能力
显示当前页面绑定的模块,以及完整的业务模块树。每个 `data` / `virtual` 模块显示:
- 是否可访问;
- 标准操作:查看、新增、编辑、删除、导出;
- 已配置的业务动作,例如审核、结算;
- 当前模块是否被哪些菜单使用。
选择“海运”父节点时,界面可以全选或取消全部子模块,并使用半选状态表示部分授权。这个操作只是批量生成明确的模块授权行,不保存“沿关系自动继承”的隐式规则。
#### 字段和数据范围
管理员先选择一个具体模块,再配置字段权限和数据范围。不要在“海运”父节点上混合展示主单、装箱和费用的字段,因为这些字段属于不同模块,数据范围也可能不同。
页面绑定关系可以提供一个“按页面补齐模块权限”的快捷操作:管理员选择菜单后,系统列出该页面使用的主模块、明细模块和辅助模块,管理员确认后一次性生成模块权限。这个操作是显式的授权动作,菜单绑定本身不自动授权。
页面确实依赖某个模块时,应在页面配置中标记该模块为必需模块;可选模块则在用户没有权限时隐藏对应面板。必需/可选属于页面使用配置,不从 `s_relation` 的数据关系推导。
授权效果示例:
| 用户 | 菜单入口 | 模块权限 | 页面效果 |
| --- | --- | --- | --- |
| 张三 | 海运工作台 | 主单、装箱 | 能进入页面,费用面板隐藏或不可用 |
| 李四 | 海运工作台 | 主单、装箱、费用 | 能使用完整页面 |
| 王五 | 无海运菜单 | 主单 | 不能从海运菜单进入,但模块接口仍按模块权限统一校验 |
页面是否显示可以由“菜单权限 + 至少一个可访问的必需模块”共同决定;真正的查询、保存、删除和动作权限仍必须在后端按模块执行。
### 3.5 默认使用“业务授权方案”,隐藏技术细节
如果每个用户都要先分配菜单,再逐个分配主单、装箱、费用和动作,业务管理员会觉得权限系统过于复杂。因此默认授权入口不直接展示三套独立权限表,而是展示业务人员能理解的授权方案:
```text
海运
├── 未授权
├── 只读
├── 经办
└── 主管
```
方案的含义由系统管理员预先定义。例如:
| 方案 | 菜单入口 | 海运主单 | 装箱信息 | 费用 |
| --- | --- | --- | --- | --- |
| 只读 | 海运工作台、海运单列表 | 查看、导出 | 查看 | 查看 |
| 经办 | 海运工作台、海运单列表 | 查看、新增、编辑、导出 | 查看、编辑 | 查看 |
| 主管 | 海运全部相关页面 | 全部标准操作和业务动作 | 全部标准操作 | 查看、编辑、导出、结算 |
管理员选择“海运·经办”后,界面先展示授权预览:
```text
将授予:
菜单:海运工作台、海运单列表
模块:海运主单(查看/新增/编辑/导出)
装箱信息(查看/编辑)
费用(查看)
```
确认后,在同一事务中写入菜单权限和模块/动作权限。这里的“方案”只是管理员操作的简化入口,运行时的真实权限仍然按模块、动作、字段和数据范围校验;菜单绑定不会在运行时隐式授予模块权限。
默认页面只展示业务名称和方案,不展示模块编码、字段编码和 `s_relation`。授权预览中的“主单、装箱、费用”依赖来自 `s_menu_module` 的页面模块清单,不从数据关系推导。
高级管理员可以展开“详细调整”:单独修改某个模块、动作、字段或数据范围。详细调整生成同一套底层权限记录,不另建一套与方案互相覆盖的规则。方案变更后,应显示变更前后差异并要求确认。
第一期不必新增复杂的角色继承体系。可以先把方案作为权限页面上的预置操作,直接物化为现有 `s_power` / `s_user_power` 等权限行。出现多个用户需要相同授权时,再把方案独立为可复用的权限模板。
## 四、目标表设计
### 4.1 `s_module`:模块组织树
继续使用现有字段:
- `b_id`:模块编码。
- `b_parent_id`:直接父模块,树关系的唯一权威来源。
- `b_depth`、`b_path`:由业务层维护的派生字段。
- `b_module_type`:`module`、`data`、`virtual`。
约束:
- `module` 可以包含下级 `module`、`data` 和 `virtual`。
- `data`、`virtual` 默认作为叶子节点。
- 不允许自指和循环。
- 移动模块时,在同一事务内更新当前节点及全部后代的 `b_depth`、`b_path`。
- 不从 `s_relation` 推导 `b_parent_id`。
### 4.2 `s_menu`、`s_menu_module`:菜单与模块绑定
`s_menu` 继续使用 `b_parent_id` 建立菜单树。
`s_menu_module` 继续使用联合主键:
```text
(b_menu_id, b_module_id)
```
绑定规则:
- `directory`、`external` 不强制绑定模块。
- `page` 可以绑定一个或多个 `data` / `virtual` 模块。
- `module` 类型只作为分类和上下文,不直接进入菜单模块绑定。
- UI 可以按模块树批量勾选,但保存时写入明确的 `s_menu_module` 行。
- 删除模块前检查并清理其菜单绑定,不能通过数据关系级联推断菜单绑定。
### 4.3 `s_relation`:数据关系
建议将管理页面名称改为“数据关系”,表继续使用字段级复合键:
```text
b_source_module_id
b_source_field
b_target_module_id
b_target_field
b_relation_type -- one_to_one / one_to_many / many_to_one
b_canuse
b_xh
```
它只回答:
> 哪个模块的哪个字段与另一个模块的哪个字段存在结构关系?
它不保存:
- 菜单编码。
- 用户编码。
- 权限编码。
- 模块树父节点。
- 菜单显示顺序。
#### 删除行为的处理
当前通用删除服务已经依赖 `b_on_delete`,因此建议采用兼容方案:
- 数据库中暂时保留 `s_relation.b_on_delete`。
- 关系面板只显示该列或提供跳转,不作为菜单权限关系编辑。
- 唯一可编辑入口放到“删除策略 → 关联删除处理”。
- 只保留 `restrict`、`cascade`、`none` 三值。
- 不根据 `b_relation_type` 自动推断删除行为。
如果后续确认所有模块都由专用 SQL 或存储过程删除,则可以迁移掉 `b_on_delete`,让 `s_relation` 只保留结构关系。
### 4.4 `s_delete_rule`:业务删除条件
`s_delete_rule` 只处理“什么情况下不允许删除”,不再保存数据关系键:
```text
b_id
b_module_id
b_sql -- 只读检查 SQL,命中行即拒绝删除
b_message
b_xh
b_canuse
```
适合配置:
- 已审核单据不能删除。
- 已开票记录不能删除。
- 已生成下游业务单据不能删除。
- 状态或跨表条件不满足时不能删除。
SQL 只做检查,不允许配置 SQL 直接修改或删除业务数据。实际物理删除继续由后端事务服务负责。
## 五、前端改造方案
### 5.1 模块管理页面
建议将业务能力分节调整为:
```text
基础
字段
界面
数据关系
删除策略
自动编码
权限
多语言
```
其中:
- “数据关系”只维护字段关系。
- “删除策略”包含“关联删除处理”和“删除 SQL 规则”。
- “权限”只维护 `s_power`,不展示删除配置。
- “数据关系”只对 `data` 模块开放。
### 5.2 数据关系面板
保留:
- 新增关系。
- 源字段选择。
- 目标模块、目标字段选择。
- 关系类型。
- 启用/停用。
- 排序号。
- 当前模块作为源的关系编辑。
- 其他模块指向当前模块的关系只读展示。
调整:
- 面板标题从“模块关联”改为“数据关系”。
- 删除处理列改为只读并提供“前往删除策略”的入口,或者完全移入删除策略面板。
- 不出现菜单、权限、用户等字段。
- 保存前校验模块、字段存在,字段类型可匹配,关系不能重复。
### 5.3 菜单管理页面
- 左侧继续展示模块树。
- 勾选 `module` 分类节点时,展开其下的 `data` / `virtual` 模块。
- 保存时只提交 `s_menu_module` 的增删行。
- 菜单页面绑定的模块不反向修改模块树。
- 模块被多个菜单使用时,只维护多条菜单绑定,不复制模块。
### 5.4 权限面板
- 模块使用权限继续以 `module.{moduleCode}` 为主。
- 业务动作使用 `action.{moduleCode}.{action}`。
- 菜单权限只控制入口。
- 不因 `s_relation` 存在而自动给用户增加目标模块权限。
- 如果未来需要继承,只允许从 `s_module.b_parent_id` 的组织树计算,并明确配置继承边界;不使用字段关系推导。
## 六、删除实现取舍
### 6.1 当前阶段推荐方案
保留通用删除服务:
```text
模块删除
→ 根据 s_relation.b_on_delete 展开 cascade
→ 检查 restrict 外部引用
→ 执行 s_delete_rule SQL 检查
→ 同一事务内物理删除
```
这样适合模块数量较多、希望新增模块主要靠配置的场景。`s_relation` 为删除服务提供结构信息,`s_delete_rule` 为业务条件提供 SQL 出口,两者职责明确。
### 6.2 SQL 直接处理的适用边界
SQL 适合:
- 复杂查询和跨表判断。
- 固定模块的特殊清理逻辑。
- 数据库外键的 `CASCADE` / `NO ACTION`(物理结构稳定时)。
不建议:
- 让管理员配置任意 `DELETE SQL` 作为通用删除入口。
- 用 SQL 文本判断用户菜单权限。
- 用 SQL 关系自动推导模块父子和权限继承。
如果未来选择“完全模块专用 SQL 删除”,迁移顺序应是:先为每个模块补齐删除服务和测试,再停止通用删除服务读取 `s_relation`,最后删除 `b_on_delete`,不能先删元数据再补删除逻辑。
## 七、迁移步骤
### 阶段 0:盘点和校验
1. 统计现有 `s_relation` 行,确认每行的源模块、源字段、目标模块、目标字段真实存在。
2. 检查重复关系、自引用、无效模块和无效字段。
3. 盘点旧系统 `s_modulelink` 或其他模块链接数据,确认哪些是菜单绑定,哪些是数据关系,禁止直接整表迁移到 `s_relation`。
4. 统计菜单、模块、权限之间的现有绑定,找出没有模块权限但已有菜单入口的情况。
### 阶段 1:先改语义和界面
1. 将模块管理中的“模块关联”改为“数据关系”。
2. 移除它对菜单权限的描述。
3. 在菜单管理中明确展示 `s_menu_module` 绑定。
4. 增加“删除策略”分组,保证 `b_on_delete` 只有一个编辑入口。
5. 不立即删除数据库列和旧数据,保持兼容。
### 阶段 2:拆分权限计算
1. 路由和侧栏只根据菜单权限及菜单绑定判断入口。
2. 页面内模块操作根据 `s_power` / `s_user_power` 独立判断。
3. 字段和数据范围继续走独立权限表。
4. 全局搜索确认权限代码没有读取 `s_relation`。
### 阶段 3:收敛删除策略
1. 删除前条件统一读取 `s_delete_rule`。
2. 关联删除统一读取 `s_relation.b_on_delete`。
3. 删除列表入口和 `saveobjt` 使用同一删除服务。
4. 对 `cascade` 做环路、深度、重复记录和事务回滚测试。
5. 对 `restrict` 返回引用模块、字段和示例记录,便于用户处理。
### 阶段 4:清理旧概念
只有在兼容数据完成迁移后,才删除:
- 旧的菜单关系字段。
- 旧的引用规则或重复删除配置。
- 模块关联面板中的权限文案。
- 不再使用的关系推导逻辑。
## 八、验收标准
### 模块树
- 模块只能有一个直接父节点。
- 不能自引用或形成循环。
- 移动节点后,所有后代的 `b_path`、`b_depth` 正确。
- 数据字段关系不会改变模块树。
### 菜单和权限
- 一个菜单可以绑定多个模块。
- 一个模块可以被多个菜单复用。
- 勾选模块分类节点只是批量操作,保存后仍是明确绑定行。
- 只有菜单权限不能直接操作模块数据。
- 有数据关系的两个模块不会自动互相获得权限。
### 数据关系
- 关系字段存在且类型可匹配。
- 关系重复时保存失败。
- 反向关系可查看,但不会重复提交或误删。
- `s_relation` 不包含菜单、用户和权限字段。
### 删除
- `cascade` 能正确删除拥有关系的子记录。
- `restrict` 能阻止外部引用导致的父记录删除。
- `none` 不自动处理关联记录,并在配置界面显示风险提示。
- 删除 SQL 命中时整批回滚。
- 技术表使用 `target=direct` 时不读取模块关系。
- 任何检查失败时,不留下部分删除结果。
## 九、文件影响范围
### 数据库和脚本
- `sql/fms_core.sql`:确认 `s_module`、`s_menu`、`s_menu_module`、`s_relation`、`s_power` 的职责注释和字段。
- `sql/fms_delete_rule.sql`:只保留删除策略迁移和 `s_delete_rule` 初始化。
- 新增一份关系数据校验/迁移脚本,先报告问题,不直接静默修复。
### 前端
- `fms-vue/src/views/module/module-management/index.vue`:调整分节名称和数据关系/删除策略的编排。
- `ModuleRelationPanel.vue`:改名、缩减职责、移除权限语义。
- `ModuleRulePanel.vue`:作为删除 SQL 规则面板保留。
- `fms-vue/src/views/module/menu-management/index.vue`:继续以 `s_menu_module` 保存菜单绑定。
- `stores/permissions.js`:确认权限计算不读取 `s_relation`。
### 后端
- `DataDeleteService.java`:继续只把 `s_relation` 当作数据关系来源。
- `SqlPermissionService.java`:权限只读取权限表,不通过数据关系推导。
- 菜单接口和模块接口分别校验自己的树结构,不能共用一套父子关系处理。
## 十、需要确认的唯一决策
本计划默认保留通用删除服务,因此保留 `s_relation.b_on_delete` 作为底层删除策略字段,但把编辑入口移到“删除策略”。
如果决定所有业务模块都用专用 SQL 或存储过程处理删除,则可以选择另一条路线:
```text
s_relation 只保留数据结构关系
s_delete_rule 保留删除前检查
模块删除由各自服务/存储过程完成
```
这条路线的配置更少,但新增模块不能只靠元数据完成删除行为,需要额外编写和测试模块删除逻辑。
在当前 FMS 已经存在通用 `DataDeleteService`、模块数量较多的前提下,建议先采用默认方案:**保留数据关系,拆出权限语义,删除策略单独管理,暂不改成纯 SQL 删除。**
## 十一、原型演示
对应的交互原型位于 [权限分配原型.html](权限分配原型.html),直接用浏览器打开即可查看。
原型按常见企业系统的授权路径组织:
1. 左侧切换用户,顶部查看当前授权来源和保存状态。
2. 先选择海运、空运或财务业务域。
3. 默认选择“未授权、只读、经办、主管”等业务方案,右侧立即预览菜单入口、模块和动作。
4. 需要精细控制时,在海运业务中打开“详细权限调整”,分别配置菜单、模块、字段和数据范围。
5. 用“预览最终权限”查看后端实际会收到的授权结果,用“撤销修改”验证未保存变更不会直接生效。
原型中的角色方案只是降低管理员理解成本的操作入口,底层仍然落成菜单、模块和动作权限;业务域和模块之间的字段关系没有被用来推导权限。