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