Files
workspace/code/fms/FMS模块设计评审.md
T
2026-09-19 21:17:02 +08:00

238 lines
14 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 模块设计评审
> 版本:v1.2。评审对象:`FMS新系统核心表结构设计.md`(模块 / 字段 / 界面配置 / 模块关系 / 权限)。
> 关联:`FMS删除策略重构设计.md`(当前删除策略架构)、`开发规范.md`、`CODEBUDDY.md`。
> 一句话:**骨架是对的,改三处即可——关系删除行为一列化、配置分节按业务问题分、规则表达对齐权限体系。**
>
> v1.1 修订:问题一原建议「加 `b_relation_kind` 语义列 + 按语义推导默认行为」,定案为不加语义列、单列 `b_on_delete` 表达删除行为。该部分已由当前删除策略重构重新收敛为三值方案。
>
> v1.2 修订:第八节第 3、4 项定案——中间表模块统一挂业务域节点下(规范已写入 `FMS新系统核心表结构设计.md` §4.3);模块管理分节调整随删除规则前端改造同一批实施。
> 当前实施以 `FMS删除策略重构设计.md` 为准。本评审中关于四值删除行为、归档、勾条件、引用规则和 `s_rule` 的章节保留为历史讨论,不再作为实施规格。
---
## 一、总评
`s_module` / `s_field` / `s_module_schema` 三张表 + 权限四张表,这个骨架抓住了正确的抽象层次:
- **模块树(`s_module`)表达业务组织**,不表达数据关系;
- **`s_relation` 表达数据关系**(主子表、引用、多对多);
- **`s_field` 只存业务元数据**,物理结构现场读表——这一条非常克制,避开了「元数据与真实表结构不同步」的经典泥潭;
- **UI 配置整份进 JSON**,不建字段分组表;
- **权限四张表各答一个问题**(有哪些权限点 / 用户有哪些 / 字段能做什么 / 能看到哪些数据)。
最大优点:**模块之间的差异全部是数据,不是代码**。新增业务模块 = 插几行配置,不改后端。这正是设计文档开头「以 SQL 为核心驱动」要的效果。
因此本评审的结论是**局部调整,不是推倒重来**。下面按严重程度列出问题。
---
## 二、问题一:`s_relation` 承担了太多角色(最大结构问题)
### 现状
`s_relation` 同时表达三种语义完全不同的关系,但**表里没有任何一列区分它们**:
| 语义 | 例子 | 生命周期特点 |
| --- | --- | --- |
| 主子表 | 报销单 → 费用明细 | 绑定,删主单应连带 |
| 引用 | 费用 → 币种、客户 | 保护性,被引用不应删 |
| 多对多 | 中间表模块 + 两条 `one_to_many` | 结构关系 |
`b_relation_type`(`one_to_one` / `one_to_many` / `many_to_one`)说的是**基数**,不是**语义**。`one_to_many` 既可能是主子表(该连带删),也可能是引用(该 restrict),系统无从判断。
### 后果
因为表本身不知道「这条关系是什么性质」,只能让配置者**每条关系手工声明一遍删除行为**——这就是「模块关联面板三列(关系类型 / 删除连带 / 有引用时拒绝删除)太麻烦」的根因。
同时派生出两个具体毛病:
1. **语义矛盾可被配置出来**:`b_on_delete = cascade` 与「有引用时拒绝删除」可同时勾上,等于「删源连带删目标」+「但目标不许删」。当前没有任何校验拦截。
2. **方向语义不统一**:`b_on_delete` 的方向是「删**源**时对**目标**怎么办」(src→tgt),而引用规则保护的是 **target**。两者方向不同却并列摆放,极易配错。
### 建议(v1.1 定案:单列 `b_on_delete`,不加语义列)
初版评审建议加 `b_relation_kind` 语义列、按语义推导默认行为。进一步论证后**砍掉语义列**,理由:
1. **基数已隐含方向**:`many_to_one` 外键在源字段、`one_to_many` 外键在目标字段——「谁是被引用方」系统总能判定,语义列不提供方向信息;
2. **语义与行为同轴**:`detail` 的可选行为就是 cascade / archive / none,`reference` 的就是 restrict / none——第二列锁死在第一列的邻域内,不产生新信息;
3. **唯一只有配置者知道的事**是四选一的行为本身(被引用方是否独占拥有引用方),单列直接表达它;
4. 与 `开发规范.md`「为真实出现的第二个使用方而抽象」一致——不为「表单将来可能用语义做别的」预留列。
最终结构(历史方案;当前方案详见 `FMS删除策略重构设计.md`):
```sql
alter table dbo.s_relation add
b_on_delete varchar(20) not null default 'restrict';
-- restrict(默认) 被引用时禁止删除 / cascade 连带删除 / archive 连带归档 / none 不处理
```
四个取值共享同一方向语义——**「删除被引用方时,引用方怎么办」**(即 SQL `ON DELETE` 的口径),v1 的「删源 vs 删目标」两套口径随之消失:
| 值 | 含义 | 典型场景 |
| --- | --- | --- |
| `restrict`(**默认**) | 仍有引用方记录时,拒绝删除被引用方 | 费用→客户:客户被引用删不掉 |
| `cascade` | 引用方记录一起物理删除 | 报销单→费用明细:删单连带删明细 |
| `archive` | 引用方记录打归档标记 | 删报销单,明细归档可追溯 |
| `none` | 不处理 | 日志等确实无关的关联 |
**关键收益:**
- **默认即防护**:引用关系(系统里的大多数)零配置,安全方向从「记得配防护」反转为「记得放开」——「三列太麻烦」从根上消失;
- **一列取代三个栏位**:关系面板删除相关配置收敛为一个真列;v1 的「有引用时拒绝删除」假列(实际生成 `s_rule` 行)及其全部联动补丁消失;
- **非法组合在类型层面不存在**:v1 可同时配出「cascade + 引用规则」的自相矛盾,单列四值后不可能。
**关于推导**(初版备选方案的处置):从「目标模块是否 `data` 叶子 + 源字段是否指向目标 id」推导语义属于隐式魔法,配错难排查。定案把两件事分开:**被引用方判定用基数**(客观事实,不配置),**行为选择用显式列**(配置者判断,直存值)——各归其位,互不推导。
---
## 三、问题二:「能做什么」被切成两组不相干的东西
### 现状
模块管理页当前分节(`views/module/module-management/index.vue`):
```text
数据结构 → 字段定义
界面配置 → 表单设计 / 列表配置 / 查询配置
业务能力 → 自动编码 / 权限 / 模块关联 / 删除规则
高级设置 → 多语言
```
这是**按「配置的载体」分,而不是按「业务问题」分**。于是同一个业务问题被切到多处:
- 「这个模块的记录在什么情况下不许删」→ **删除规则**
- 「这条记录被别人引用会怎样」→ **模块关联**
- 「谁能用、能看哪些字段、能看哪些数据」→ **权限**
而用户在真实场景里问的是一个整体问题:**「这张单据在什么情况下会被拦下来?」** 答案分散在三个面板里。
**这正是「删除规则要在模块关联和删除规则两处维护」困惑的根因**——不是删除规则本身的问题,是分节维度的问题。
### 建议
按**「改这个模块会影响什么」**重新分节:
```text
基础 → 模块定义(类型、查询来源、保存目标、主键)
字段 → s_field
界面 → view / edit / query 三份 JSON
数据行为 → 模块关联(含删除行为) + 删除规则 + 自动编码 ← 「数据怎么被约束、怎么被生成」
访问控制 → 权限点 + 字段权限 + 数据范围 ← 「谁能做什么」
```
- 「删除时怎么办」「被引用怎么办」「编号怎么生成」同属**数据行为**,是同一问题的不同侧面,配置者在一处想清楚;
- **访问控制**自成一个整体,因为权限四张表本就是一个体系。
---
## 四、问题三:规则的表达方式不统一
### 现状
`s_rule` 用 `b_predicate` 三态推断类型:空 = 引用规则 / `{` 开头 = 勾条件 JSON / 其余 = SQL 文本。
这在整体设计中是**问题二的症状**——规则被拆到「删除规则」和「模块关联」两个面板,为共用一张表才需要三态推断。附带两个技术缺陷:
1. **判别依据靠猜**:`ruleUtils.js` 的 `parseRulePredicate` 以「首字符是不是 `{`」区分 JSON 与 SQL,依赖「SQL 不以 `{` 开头」这个未声明约定。
2. **作用域用拼接串**:`b_scope_id` 存 `源模块|源字段|目标模块|目标字段`,无法有效索引、无法校验、编码含 `|` 时错位。设计文档原写 `bigint`,实现改为 `varchar(250)` 拼接串,**文档未同步**。
### 已在文档内部存在的正确答案
`FMS新系统核心表结构设计.md` §14.5 对数据范围给出了正确判断:
> 后续如规则体系成熟,优先将 `b_condition_sql` 升级为结构化条件(字段 / 操作符 / 值的 AST),避免让 SQL 成为权限系统的核心表达方式。
**这个判断完全正确,但只写在权限那一节。** 数据范围有 `b_scope_type` 五类枚举 + `b_scope_field` + `b_condition_sql` 的完整结构,而删除规则是「条件 JSON 与 SQL 塞同一列」。**两个体系面对同一类问题(「什么条件下允许/禁止」),表达水平却不一致。**
### 建议
对齐权限体系的水平,`s_rule` 收敛为只管**模块级业务条件**:
- 加显式 `b_kind` 列(`condition` / `sql`),判别依据存进数据而非靠猜;
- `b_scope_id` 拼接串换成直白的 `b_module_id`;
- 引用约束**移出 `s_rule`**,改由 `s_relation.b_on_delete = 'restrict'`(默认)表达;
- 去掉 `b_hook`(一期只有 `delete.pre` 一种取值,属过早抽象);
- 去掉 `b_scope_type`(同上)。
该历史方案已由 `FMS删除策略重构设计.md` 取代。
---
## 五、问题四:模块树与数据关系边界有摩擦
### 现状
§4.3 已明确定清边界,这是好的:
> `data` 和 `virtual` 默认作为叶子节点。
> 主子表和引用关系通过 `s_relation` 表达,不使用模块树表达。
但 §1.7 同一体系中又说:
> 多对多通过中间表模块加两条 `one_to_many` 表达。
**中间表模块是 `data` 模块(按 §4.3 应为叶子),但它天然是主子表的子节点。** 到底挂在模块树的哪个位置,规范没有交代。
### 风险
模块树一旦混入数据关系,`b_path` 前缀查询、菜单挂载、权限继承都会被污染。而权限继承(§4.2 提到 `module` 可作「权限继承的边界」)对树的纯净度尤其敏感。
### 建议
补一句规范,明确中间表模块在模块树里的挂载位置。**已定案并写入** `FMS新系统核心表结构设计.md` §4.3:中间表模块(`data`)统一挂在所属业务域 `module` 节点下,与参与多对多的主表模块平级,不挂在任一主表模块下。**这是表述缺口,不是结构缺陷,一句话即可闭合。**
---
## 六、问题五:实现一致性缺口
非设计问题,但影响判断「配置是否真的生效」。
| 设计/文档有 | 实际状态 |
| --- | --- |
| 删除策略引擎(`FMS删除策略重构设计.md`) | **后端零实现**:删除计划、SQL 规则检查、`deleteobj` 和 `b_on_delete` 执行链路仍待实现 |
| `DataSaveService` 的 delete 分支接入规则检查 | 未实现,`DataSaveService.java:470` 直接 `dbUtils.delete(...)` |
| `s_rule.b_scope_id` 为 `bigint`(v1 设计) | 实现为拼接串 `varchar(250)`;v2.0 已整体重新定义表结构,该差异随重建消除 |
| 删除「先预览后确认」(设计 §4) | 列表页 `deleteSelected` 直接调 `saveObjectApi`,无预览/确认两步 |
| `save.pre` / `b_hook` 预留位 | 只有 `delete.pre` 一种取值,预留位无实际内容 |
**风险提示**:删除策略未实现时,界面上配置的删除规则**不生效**。这比没有该功能更危险——它给配置者虚假的安全感。详见 `FMS删除策略重构设计.md`。
---
## 七、建议的落地顺序
1. **`s_relation` 加 `b_on_delete`**(三值、默认 `restrict`),按 `FMS删除策略重构设计.md` 的架构调整;
2. **模块管理分节改为「数据行为 / 访问控制」**,按业务问题而非载体划分;删除策略统一为关联删除处理 + 删除 SQL 规则;
3. **删除规则专用化**:使用 `s_delete_rule`,只保留模块、SQL、提示、启用和顺序;
4. ~~补规范:明确中间表模块在模块树中的挂载位置~~ **已完成**:规范已写入 `FMS新系统核心表结构设计.md` §4.3;
5. **实现后端删除引擎**(`deleteobj` + 检查器 + 执行器 + `saveobjt` 接入)。
第 5 项与第 1–4 项独立,但**优先级最高**——它决定这套配置是否真的有用。当前建议**先做第 5 项或与之并行**,避免又一次「界面配得完整、底层不生效」。
---
## 八、待确认
| # | 事项 | 说明 |
| --- | --- | --- |
| 1 | ~~`b_relation_kind` 语义列取值是否够用~~ | **已定案(v1.1)**:不加语义列——它与删除行为同轴,不提供额外信息;被引用方由基数判定(见问题一) |
| 2 | ~~删除行为独立列还是语义推导~~ | **已定案(v1.1)**:独立列 `b_on_delete` 直存四值、默认 `restrict`,不做推导(见问题一) |
| 3 | ~~中间表模块的实际挂载形态~~ | **已定案(v1.2)**:统一挂业务域 `module` 节点下、与主表模块平级;规范已写入核心表结构设计 §4.3(见问题四) |
| 4 | ~~分节调整是否影响既有页面路由与用户习惯~~ | **已定案(v1.2)**:面板组件、section key、路由、数据全不动,只改 `configGroups` 分组;系统未上线且该页 `adminOnly`,无用户习惯包袱;与删除规则前端改造同一批实施(见问题二) |
---
## 九、明确不变的部分
以下设计经评审**确认合理,不建议改动**:
- `s_module` 三类型(`module` / `data` / `virtual`)与模块树邻接表 + `b_depth` / `b_path` 冗余方案;
- `s_field` 只存业务元数据、物理结构现场读表;
- UI 配置整份 JSON、字段分组作为 JSON 节点而非独立表;
- 两层继承(`s_module_schema` 系统默认 → `s_user_module_pref` 个人增量),个人层只存偏离量;
- 权限四张表的职责划分,以及「菜单/模块/动作统一收在 `s_power`,字段权限与数据范围不进 `s_power`」的边界;
- 不使用外键、`CHECK`、触发器,约束统一交业务层;
- 查询来源优先级(`b_query_sql` > `b_view_table`)与只读 SQL 边界。