236 lines
14 KiB
Markdown
236 lines
14 KiB
Markdown
# FMS 模块设计评审
|
||
|
||
> 版本:v1.2。评审对象:`FMS新系统核心表结构设计.md`(模块 / 字段 / 界面配置 / 模块关系 / 权限)。
|
||
> 关联:`FMS删除规则引擎设计.md`(已按本评审定案修订为 v2.0)、`开发规范.md`、`CODEBUDDY.md`。
|
||
> 一句话:**骨架是对的,改三处即可——关系删除行为一列化、配置分节按业务问题分、规则表达对齐权限体系。**
|
||
>
|
||
> v1.1 修订:问题一原建议「加 `b_relation_kind` 语义列 + 按语义推导默认行为」,定案为**不加语义列、单列 `b_on_delete` 四值**——语义列与删除行为同轴,不提供额外信息;被引用方由基数判定。`FMS删除规则引擎设计.md` 已同步修订为 v2.0。
|
||
>
|
||
> v1.2 修订:第八节第 3、4 项定案——中间表模块统一挂业务域节点下(规范已写入 `FMS新系统核心表结构设计.md` §4.3);模块管理分节调整随删除规则前端改造同一批实施。
|
||
|
||
---
|
||
|
||
## 一、总评
|
||
|
||
`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` v2.0 §3):
|
||
|
||
```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`(v2.0)。
|
||
|
||
---
|
||
|
||
## 五、问题四:模块树与数据关系边界有摩擦
|
||
|
||
### 现状
|
||
|
||
§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` §6.4/§6.5 整节) | **后端零实现**:`s_rule` / `deleteobj` / `b_on_delete` / `b_archived` 在 `fms-api` 中全部 grep 无匹配 |
|
||
| `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` v2.0 的表结构调整;
|
||
2. **模块管理分节改为「数据行为 / 访问控制」**,按业务问题而非载体划分;与删除规则前端改造(`FMS删除规则引擎设计.md` §8 步骤 5)同一批实施——两者都要动 `module-management/index.vue`,不单独安排;
|
||
3. **`s_rule` 结构化**:显式 `b_kind`、直白 `b_module_id`、去掉预留位;
|
||
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 边界。
|