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

23 KiB
Raw Blame History

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。核心表设计 模块设计评审

二、当前问题

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 三棵树、两类关系

模块组织树
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  → 数据范围

菜单绑定模块不等于授予模块权限。原因有三点:

  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。授权页面以业务域和模块树呈现:

海运(业务域,只做分组)
├── 海运主单(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:盘点和校验

  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 或存储过程处理删除,则可以选择另一条路线:

s_relation 只保留数据结构关系
s_delete_rule 保留删除前检查
模块删除由各自服务/存储过程完成

这条路线的配置更少,但新增模块不能只靠元数据完成删除行为,需要额外编写和测试模块删除逻辑。

在当前 FMS 已经存在通用 DataDeleteService、模块数量较多的前提下,建议先采用默认方案:保留数据关系,拆出权限语义,删除策略单独管理,暂不改成纯 SQL 删除。

十一、原型演示

对应的交互原型位于 权限分配原型.html,直接用浏览器打开即可查看。

原型按常见企业系统的授权路径组织:

  1. 左侧切换用户,顶部查看当前授权来源和保存状态。
  2. 先选择海运、空运或财务业务域。
  3. 默认选择“未授权、只读、经办、主管”等业务方案,右侧立即预览菜单入口、模块和动作。
  4. 需要精细控制时,在海运业务中打开“详细权限调整”,分别配置菜单、模块、字段和数据范围。
  5. 用“预览最终权限”查看后端实际会收到的授权结果,用“撤销修改”验证未保存变更不会直接生效。

原型中的角色方案只是降低管理员理解成本的操作入口,底层仍然落成菜单、模块和动作权限;业务域和模块之间的字段关系没有被用来推导权限。