# 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. 用“预览最终权限”查看后端实际会收到的授权结果,用“撤销修改”验证未保存变更不会直接生效。 原型中的角色方案只是降低管理员理解成本的操作入口,底层仍然落成菜单、模块和动作权限;业务域和模块之间的字段关系没有被用来推导权限。