20260917231331
This commit is contained in:
1 parent
5cbf745bbf
commit
f56e70b050
4 files changed
+454
-100
No files matched your search
+145
-100
@@ -1,7 +1,14 @@
|
||||
# FMS 删除规则设计
|
||||
|
||||
> 版本:v1.0。关联:`FMS新系统核心表结构设计.md`。
|
||||
> 版本:v2.0(取代 v1)。关联:`FMS新系统核心表结构设计.md`(§9 模块关系)、`FMS模块设计评审.md`。
|
||||
> 一句话:**删之前先查一查,查不过就拒绝;查什么、怎么查,由 IT 配置,不写代码。**
|
||||
>
|
||||
> v2 相对 v1 的变化(动因见 `FMS模块设计评审.md` 问题一/问题三):
|
||||
> - **引用规则退出 `s_rule`**:原「勾引用」由 `s_relation.b_on_delete = 'restrict'` 表达,关系面板只配一列;
|
||||
> - **`s_rule` 收敛为模块级**:去掉 `b_scope_type`、`b_scope_id`(拼接串)、`b_hook`,加 `b_kind` 显式区分勾条件与写 SQL;
|
||||
> - **方向语义统一**:四个取值都描述「删除**被引用方**时,**引用方**怎么办」,被引用方由基数判定,不再有删源/删目标两套口径;
|
||||
> - **默认值 `restrict`**:被引用即不可删成为默认,多数关系零配置。
|
||||
> - 库中无存量数据,全部按新结构直接建,不做迁移。
|
||||
|
||||
## 这个设计解决什么问题
|
||||
|
||||
@@ -15,26 +22,31 @@
|
||||
|
||||
以前这些规矩要么写死在代码里(改一条要发版),要么干脆没有(误删了才发现)。现在的做法:**IT 在界面上配置,配完立即生效**。
|
||||
|
||||
规矩分两类,配在两个地方:
|
||||
|
||||
- **记录自身什么状态不许删** → 模块上配「删除规则」(勾条件 / 写 SQL);
|
||||
- **被别人引用着不许删、删主单连带删明细** → 关系上选一个「删除行为」(一列四值,默认即防护)。
|
||||
|
||||
## 1. 整体图景
|
||||
|
||||
一个用户点「删除」,系统背后做四件事:
|
||||
|
||||
```
|
||||
1. 找出连带要删的记录(删报销单 → 连费用明细、附件一起)
|
||||
1. 找出连带要删的记录(删报销单 → 连费用明细一起)
|
||||
2. 把配置的所有删除规则挨条查一遍
|
||||
3. 任何一条不过 → 整批拒绝,告诉用户为什么
|
||||
4. 全过 → 先弹预览给用户确认,再一个事务里一起删
|
||||
```
|
||||
|
||||
IT 要做的事只有一件:**配规则**。配的地方在模块管理。
|
||||
IT 要做的事只有两件:模块上**配条件规则**,关系上**选删除行为**。配的地方都在模块管理。
|
||||
|
||||
## 2. 规则:一条就是一句「什么情况不许删」
|
||||
## 2. 模块规则:一条就是一句「什么情况不许删」
|
||||
|
||||
每条规则就是一句话:
|
||||
|
||||
> **当【什么条件】时,不许删,提示【什么话】**
|
||||
|
||||
规则有三种写法,按配的难度从易到难:
|
||||
规则有两种写法,按配的难度从易到难:
|
||||
|
||||
### 2.1 勾条件(日常用这个,占绝大多数)
|
||||
|
||||
@@ -55,15 +67,7 @@ IT 要做的事只有一件:**配规则**。配的地方在模块管理。
|
||||
|
||||
会配查询条件的人,零学习成本。
|
||||
|
||||
### 2.2 勾引用(防误删最常用)
|
||||
|
||||
在**关系配置**上勾一下「有引用时拒绝删除」。
|
||||
|
||||
效果:这模块的记录只要被别的单据引用着(费用被账单引用),就删不掉,提示「费用【海运费 ¥2,000】已被账单 ZD-2026-001 引用」。
|
||||
|
||||
模块的删除规则 tab 里能看到这个勾的只读展示和跳转,不会迷路。
|
||||
|
||||
### 2.3 写 SQL(兜底,藏在角落)
|
||||
### 2.2 写 SQL(兜底,藏在角落)
|
||||
|
||||
界面勾不出来的判断——比如「核销金额加起来大于 0」这种要查别的表的——在规则类型的「高级」选项里直接写一句 SQL:
|
||||
|
||||
@@ -77,19 +81,49 @@ where b.b_id in (:ids) and d.writeoff > 0
|
||||
|
||||
平时用不到,会写的人自己找得到,不会打扰别人。谁改过这条 SQL、每次执行了什么,系统日志里全程有记录(内部系统,不做限制,只做留痕)。
|
||||
|
||||
## 3. 连带删除:删主单时子表跟着删
|
||||
(v1 的「勾引用」一节取消——引用保护挪到关系上,见 §3。)
|
||||
|
||||
这个不用配条件,在模块关系上选一个「删除时连带」的策略:
|
||||
## 3. 关系上的删除行为:`s_relation.b_on_delete`(一列四值)
|
||||
|
||||
| 选项 | 意思 |
|
||||
|---|---|
|
||||
| 连带删除 | 删报销单,费用明细一起物理删掉 |
|
||||
| 连带归档 | 明细不删,打个「已归档」标记,列表里不再显示,账单引用还能追溯 |
|
||||
| 不连带 | 明细自己管自己 |
|
||||
统一语义:**删除「被引用方」记录时,「引用方」记录怎么办。**
|
||||
|
||||
关键行为:
|
||||
谁是"被引用方"由基数决定,不需要额外配置:
|
||||
|
||||
- **查连带时也过规则**:删报销单连带要删费用,但费用被账单引用着 → 整个删除(包括报销单)被拒绝,提示定位到那笔费用和那个账单
|
||||
| 基数 | 外键在 | 被引用方 |
|
||||
| --- | --- | --- |
|
||||
| `many_to_one` | 源字段 | 目标 |
|
||||
| `one_to_many` | 目标字段 | 源 |
|
||||
| `one_to_one` | 目标字段(约定) | 源 |
|
||||
|
||||
四个取值:
|
||||
|
||||
| 值 | 含义 | 典型场景 |
|
||||
| --- | --- | --- |
|
||||
| `restrict`(**默认**) | 仍有引用方记录时,拒绝删除被引用方 | 费用→客户:客户被费用引用,删不掉 |
|
||||
| `cascade` | 引用方记录一起物理删除 | 报销单→费用明细:删单连带删明细 |
|
||||
| `archive` | 引用方记录打归档标记,不物理删 | 删报销单,明细归档供账单追溯 |
|
||||
| `none` | 不处理 | 日志→用户等确实无关的关联 |
|
||||
|
||||
几个例子(源→目标按基数标注被引用方):
|
||||
|
||||
| 关系 | 基数 | 被引用方 | b_on_delete | 效果 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 费用 → 客户 | many_to_one | 客户 | restrict(默认) | 客户被费用引用时删不掉 |
|
||||
| 费用类型 → 费用 | one_to_many | 费用类型 | restrict(默认) | 类型下有费用时删不掉 |
|
||||
| 报销单 → 费用明细 | one_to_many | 报销单 | cascade | 删报销单连带删明细 |
|
||||
| 报销单 → 费用明细(归档版) | one_to_many | 报销单 | archive | 删报销单,明细归档可追溯 |
|
||||
|
||||
设计要点:
|
||||
|
||||
- **默认即防护**:新关系默认 `restrict`,「主数据被引用不许删」这类最通用的规矩零配置;确实要放开才显式改 `none`。安全方向从「记得配防护」反转为「记得放开」
|
||||
- **一列取代原来的三个栏位**:v1 的 `b_on_delete`(三值)+「有引用时拒绝删除」勾选(假列,实际生成 `s_rule` 行)+ 方向口径混用,合并为**一个真列**
|
||||
- **取值即语义,无非法组合**:v1 可同时配出「cascade + 引用规则」(连带删但又不许删,自相矛盾);单列四值后该组合在类型层面不存在
|
||||
- **基数不禁取值**:是否 `cascade` 是「被引用方是否独占拥有引用方」的业务判断,只有配置者知道;把 `cascade` 配在共享主数据的关系上属于配置错误,系统不做基数层面的硬拦(内部系统,配置留痕即可),界面在选 `cascade` / `archive` 时给出确认提示
|
||||
- **`archive` 前提**:引用方表需有 `b_archived` + `b_archive_datetime` 通用列,保存时服务层校验列存在
|
||||
|
||||
连带展开的关键行为:
|
||||
|
||||
- **查连带时也过规则**:删报销单连带要删费用明细,但费用明细被账单明细引用(restrict 入边)→ 整个删除(包括报销单)被拒绝,提示定位到那笔明细和那个账单
|
||||
- **一荣俱荣一损俱损**:任何一条卡住,整批都删不了,不存在删一半的中间状态
|
||||
- 关系套关系(单→明细→附件)会自动一层层找下去;层数太多(比如配置成了环)会直接报配置错误,不会死循环
|
||||
|
||||
@@ -111,67 +145,68 @@ where b.b_id in (:ids) and d.writeoff > 0
|
||||
|
||||
**配规则**(IT 日常):
|
||||
|
||||
- 模块管理 → 找到模块 → 「删除规则」tab → 新增
|
||||
- 每条规则 3 个格子一分钟;改完立即生效,**不用发版**
|
||||
- 常见规则可以**复制到其他模块**(多选目标模块批量克隆)——「审核中不可删」这种几十个模块通用的规矩,配一次、勾一圈目标模块就完事
|
||||
- 模块管理 → 「删除规则」tab → 新增:模块级条件规则,每条 3 个格子一分钟;改完立即生效,**不用发版**
|
||||
- 模块管理 → 「模块关联」tab → 每条关系一列「删除行为」,默认"被引用时禁止删除";主子表关系显式选「连带删除」或「连带归档」
|
||||
- 常见条件规则可以**复制到其他模块**(多选目标模块批量克隆)——v2 起全部规则都是模块级、结构同构,无例外;复制时校验目标模块存在条件里用到的字段,缺字段的目标模块在确认框里点名
|
||||
|
||||
**看全貌**(排查、盘点):
|
||||
|
||||
- 系统管理 → 「业务规则」:全系统所有规则的列表——哪个模块的、什么类型、谁最后改的、什么时候。点行跳到对应模块
|
||||
- 排查「这单为什么删不掉」:错误提示里带规则名 → 全局列表找到它 → 看条件/SQL → 完事
|
||||
- 系统管理 → 「业务规则」:全系统所有模块级规则的列表——哪个模块的、什么类型、谁最后改的、什么时候。点行跳到对应模块
|
||||
- 排查「这单为什么删不掉」:错误提示里带规则内容 → 全局列表找到它 → 看条件/SQL → 完事;被引用拦下的,提示里直接带引用方单号和关系来源
|
||||
|
||||
**谁可以配**:规则的增删改走权限控制(和菜单权限同体系)。看规则不受限。
|
||||
|
||||
## 6. 数据落库(给开发看的部分)
|
||||
|
||||
### 6.1 s_rule 规则表(新增)
|
||||
### 6.1 s_relation:删除行为列
|
||||
|
||||
权威表结构见 `FMS新系统核心表结构设计.md` §9,本节只列差异:
|
||||
|
||||
```sql
|
||||
create table s_rule (
|
||||
b_id bigint not null,
|
||||
b_code varchar(50) not null, -- 稳定标识(复制/导入导出用)
|
||||
b_name nvarchar(100) not null, -- 规则名(如"审核中不可删")
|
||||
b_scope_type varchar(20) not null, -- module / relation:挂在模块还是关系上
|
||||
b_scope_id bigint not null, -- 挂在哪个模块/哪条关系上
|
||||
b_hook varchar(30) not null, -- delete.pre(一期只做这个)
|
||||
-- save.pre(将来做"已审核不能改")
|
||||
b_predicate nvarchar(max) null, -- 条件(勾条件=JSON;写SQL=SQL文本;勾引用=空)
|
||||
b_message nvarchar(500) null, -- 提示文案(写SQL的必填;勾条件的可留空自动生成)
|
||||
b_canuse int not null default 1,
|
||||
b_xh int not null default 0, -- 多条规则谁先查(并列按 b_xh, b_id)
|
||||
b_inputuser_id bigint null,
|
||||
b_inputdatetime datetime2 null default sysutcdatetime(),
|
||||
b_updateuser_id bigint null,
|
||||
b_updatedatetime datetime2 null,
|
||||
constraint pk_s_rule primary key (b_id),
|
||||
constraint ux_s_rule_code unique (b_code)
|
||||
alter table dbo.s_relation add
|
||||
b_on_delete varchar(20) not null default 'restrict';
|
||||
-- restrict(默认) 被引用时禁止删除 / cascade 连带删除 / archive 连带归档 / none 不处理
|
||||
```
|
||||
|
||||
- 语义、取值、被引用方判定见本文 §3
|
||||
- `archive` 由服务层校验引用方表存在 `b_archived` / `b_archive_datetime` 列
|
||||
- 引擎读取时只认 `b_canuse = 1` 的关系行
|
||||
- 库中无存量数据,不需要从 v1 的 `none` 默认值做迁移;`fms_core.sql`、`fms_delete_rule.sql` 脚本需按本结构同步(现脚本默认值仍是 `none`)
|
||||
|
||||
### 6.2 s_rule:模块级规则表(v2 结构)
|
||||
|
||||
```sql
|
||||
create table dbo.s_rule (
|
||||
b_id bigint not null, -- 雪花主键(新增时前端经 /data/nextid 取号)
|
||||
b_module_id varchar(50) not null, -- 所属模块编码
|
||||
b_kind varchar(20) not null, -- condition 勾条件 / sql 写 SQL
|
||||
b_predicate nvarchar(max) not null, -- condition=JSON AST(§6.3);sql=SQL 文本
|
||||
b_message nvarchar(500) null, -- condition 可留空(引擎按条件自动生成);sql 必填
|
||||
b_canuse tinyint not null default 1,
|
||||
b_xh int not null default 0,-- 执行顺序(并列按 b_xh、b_id)
|
||||
b_created_by varchar(50) null, -- 审计列由服务端维护,配置界面不提交
|
||||
b_created_at datetime2 null,
|
||||
b_updated_by varchar(50) null,
|
||||
b_updated_at datetime2 null,
|
||||
primary key (b_id)
|
||||
);
|
||||
|
||||
-- b_scope_type / b_hook 的取值范围不建 CHECK 约束,由服务层校验
|
||||
-- (与全系统「元数据合法性走服务层」的原则一致,新增取值不用改表)
|
||||
|
||||
create index ix_s_rule_scope on s_rule (b_scope_type, b_scope_id, b_hook, b_canuse, b_xh);
|
||||
create index ix_s_rule_module on dbo.s_rule (b_module_id, b_canuse, b_xh);
|
||||
```
|
||||
|
||||
要点:
|
||||
相对 v1 的变化与理由:
|
||||
|
||||
- **模块上没有任何规则 = 直接删**(无约束)
|
||||
- 想要旧「restrict 主数据」的效果?一键模板:给所有入边关系批量勾上引用规则
|
||||
- 错误类型不用存——勾条件报「状态不符」、勾引用报「被引用」、写 SQL 报自定义文案,写法本身决定了报什么
|
||||
- 提示文案只存这一列,不重复塞进条件 JSON 里
|
||||
- **删模块/删关系时,服务层把挂着的规则一起删掉**(和字段、权限点同一套清理逻辑,全系统本来就不建物理外键)
|
||||
- 删规则 = 物理删 + 日志记全文;临时不想用某条规则 → 停用(b_canuse=0),不删
|
||||
- **去掉 `b_scope_type` / `b_scope_id`**:规则只挂模块,`b_module_id` 直存模块编码。v1 的拼接串键(`源模块|源字段|目标模块|目标字段`)随之消失——它无法有效索引、无法校验、编码含 `|` 时错位,是明确的反模式
|
||||
- **去掉 `b_hook`**:一期只有 `delete.pre` 一种挂点,单值枚举列属过早抽象(`开发规范.md`「为真实出现的第二个使用方而抽象」);将来做 save.pre 时再按当时的真实需求定表形态
|
||||
- **加 `b_kind`**:v1 靠 `b_predicate` 三态推断(空=引用 / `{` 开头=JSON / 其余=SQL),判别依据靠猜且依赖「SQL 不以 `{` 开头」这个未声明约定;显式一列后判别依据存在数据里
|
||||
- `b_xh` 保留:多条规则任一不过即拒绝,顺序不影响结论,但决定错误提示的展示顺序,也支持「便宜的勾条件规则排前、贵的 SQL 规则排后」的执行优化
|
||||
|
||||
### 6.2 老表的变化
|
||||
其他要点:
|
||||
|
||||
```sql
|
||||
-- s_module:删掉 b_delete_policy(就是那个 restrict/lifecycle/hard 三选一,表达力不够,废弃)
|
||||
-- s_relation:删掉 b_delete_policy,新增:
|
||||
alter table s_relation add
|
||||
b_on_delete varchar(20) null; -- cascade连带删 / archive连带归档 / none不连带(默认)
|
||||
```
|
||||
|
||||
存量数据转换:旧 cascade→cascade;旧 archive→archive;旧 restrict→不连带+生成引用规则;旧 set-null→丢弃(本就不该有)。
|
||||
- **模块上没有任何规则 = 该模块不受条件约束**(引用保护仍由关系默认值兜着)
|
||||
- 错误类型不用存——勾条件报「状态不符」、写 SQL 报自定义文案,写法本身决定了报什么
|
||||
- **删模块时,服务层把挂着的规则一起删掉**(`where b_module_id = …`,单条件直查,与字段、权限点同一套清理逻辑,全系统本来就不建物理外键)
|
||||
- 删规则 = 物理删 + 日志记全文;临时不想用某条规则 → 停用(`b_canuse=0`),不删
|
||||
|
||||
### 6.3 勾条件那部分的格式(JSON,界面生成,人不手写)
|
||||
|
||||
@@ -197,12 +232,18 @@ alter table s_relation add
|
||||
```
|
||||
删除(模块, ids, 是否预览):
|
||||
|
||||
① 找连带:从这批 ids 出发,沿"连带删/连带归档"的关系一层层查,
|
||||
全部装进删除清单(已装过的跳过——防关系成环;超过10层报配置错误)
|
||||
② 查规则:按顺序逐条执行挂在清单上的删除规则
|
||||
勾条件 → 按条件查清单内记录,不符的记一条错误(带实际值)
|
||||
写SQL → 拿这批 ids 跑那条 SQL,查出结果每行记一条错误(全程写日志)
|
||||
勾引用 → 查有没有清单外的单据引用清单内记录,有则记错误(带引用方单号)
|
||||
① 执行器找连带:从这批 ids 出发,沿 b_on_delete ∈ {cascade, archive}
|
||||
且 b_canuse=1 的关系,把引用方记录一层层装进删除清单
|
||||
(被删的是被引用方;已装过的跳过——防关系成环;超过10层报配置错误)
|
||||
|
||||
② 检查器查规则:
|
||||
模块条件:清单内每个模块的 s_rule(b_canuse=1) 逐条执行
|
||||
勾条件 → 按条件查清单内记录,不符的记一条错误(带实际值)
|
||||
写SQL → 拿清单内该模块的 id 跑那条 SQL,查出结果每行记一条错误(全程写日志)
|
||||
引用保护:对清单内每条记录,查它作为「被引用方」的 restrict 入边
|
||||
(b_on_delete='restrict' 且 b_canuse=1 的关系)
|
||||
引用方存在、且引用方不在清单内 → 记错误(带引用方单号)
|
||||
|
||||
③ 有错误 → 整批拒绝,返回错误列表(按规则归类,每类带数量和前几条示例)
|
||||
④ 全过 → 只是预览?返回"将删除 X 张单 + Y 条明细"摘要
|
||||
真删?→ 开事务:再快速过一遍规则(防确认间隙有变化)
|
||||
@@ -227,18 +268,18 @@ alter table s_relation add
|
||||
DataSaveService.saveTable() 的 delete 分支,执行真删之前:
|
||||
|
||||
① 按表名反查模块(b_savetable = 这张表的 data 模块);查不到模块(如日志表)→跳过
|
||||
② 该模块挂有删除规则 → 调检查器,传当前 connection(同一个事务!)
|
||||
+ 待删的这批行
|
||||
② 该模块挂有规则、或它的记录存在 restrict 入边关系 → 调检查器,
|
||||
传当前 connection(同一个事务!) + 待删的这批行
|
||||
③ 任一规则不过 → 抛 BusinessException(文案就是规则生成的提示)
|
||||
→ 整个 saveobjt 回滚——主表的修改和子表的删除一起回滚,
|
||||
用户的前端草稿还在,知道哪行费用删不掉、为什么
|
||||
→ 整个 saveobjt 回滚——主表的修改和子表的删除一起回滚,
|
||||
用户的前端草稿还在,知道哪行费用删不掉、为什么
|
||||
④ 无规则或全过 → 照常执行 deletes
|
||||
```
|
||||
|
||||
三个关键点:
|
||||
|
||||
- **同事务检查**:检查器用的就是 saveobjt 自己的 connection——表单里先改了主表状态、再删子表行,规则查到的是**改过之后**的值,不会出现「检查用旧数据、删的是新数据」的错位
|
||||
- **小闭包语义**:勾引用规则检查子表行时,要把「本次 saveobjt 所有 deletes 覆盖的记录」当作一个小闭包——引用方也在被删之列就不算外部引用。场景:删主单连带删子表行(前端编排的),子表对主单的引用指向一个同批正在删除的记录,不该拦。实现:把请求数组里所有 deletes 的 id 收集起来传给检查器
|
||||
- **小闭包语义**:检查 restrict 时,要把「本次 saveobjt 所有 deletes 覆盖的记录」当作一个小闭包——引用方也在被删之列就不算外部引用。场景:删主单连带删子表行(前端编排的),子表对主单的引用指向一个同批正在删除的记录,不该拦。实现:把请求数组里所有 deletes 的 id 收集起来传给检查器
|
||||
- **不需要预览**:表单保存是「保存」动作,用户预期就是要么全成功要么报错,不做两阶段确认
|
||||
|
||||
### 6.6 接口
|
||||
@@ -256,35 +297,39 @@ POST /data/saveobjt // 不变,deletes 分支内部自动走规则检查
|
||||
|
||||
## 7. 其他约定
|
||||
|
||||
- **字段联动**:规则里用了的字段,删除该字段时会被拦住并提示规则名——不让规则悄悄失效
|
||||
- **字段联动**:条件规则里用了的字段,删除该字段时会被拦住并提示规则内容——不让规则悄悄失效
|
||||
- **配置权限**:改规则要权限点(如 `rule.edit`),和菜单权限同一套体系;SQL 不限制内容,但改它的人必须有配置权限
|
||||
- **归档标记**:连带归档要求子表有通用列 `b_archived` + `b_archive_datetime`(配归档时服务层校验列存在);通用查询默认不显示已归档行
|
||||
- **将来扩展**(表结构已留位,暂不实现):
|
||||
- 「已审核的单不能**改**」——同一套规则换个挂点(save.pre)就行,界面和错误提示全复用
|
||||
- 「删之前先自动红冲」这种要执行动作的——预留了挂 Java 代码的口子
|
||||
- 核销总额想在列表上显示成列——那是展示需求,将来单独做,跟删除无关
|
||||
- **归档标记**:`archive` 要求引用方表有通用列 `b_archived` + `b_archive_datetime`(配归档时服务层校验列存在);通用查询默认不显示已归档行
|
||||
- **将来扩展**(二期再议,**不留列**,届时按真实需求定表形态):
|
||||
- 「已审核的单不能**改**」——形态未必与删除规则同构,到时候再决定复用还是另起
|
||||
- 「删之前先自动红冲」这种要执行动作的——预留挂 Java 代码的口子
|
||||
- 核销总额想在列表上显示成列——展示需求,单独做,跟删除无关
|
||||
- **已知边界**:restrict 只覆盖 `s_relation` 里**登记过的**引用;没登记关系的表(日志、迁移脚本写的表)不在保护范围内。这是配置边界,不是数据库级引用完整性,文档与培训材料里要写明
|
||||
|
||||
## 8. 开工顺序
|
||||
|
||||
1. 建表 s_rule;删老字段、加 `b_on_delete`(存量按 §6.2 转换);前端基本信息表单同步去掉「删除策略」下拉
|
||||
1. 建表:`s_relation` 加 `b_on_delete`(四值、默认 `restrict`);`s_rule` 按 §6.2 新结构建;同步重写 `fms_core.sql`、`fms_delete_rule.sql`(现脚本仍是 v1 结构)
|
||||
2. 后端:条件(JSON → 参数化 SQL)+ 检查器 + 执行器(连带展开 → 规则 → 事务删除)
|
||||
3. **saveobjt 接入检查器**:delete 分支反查模块 → 同事务规则检查 → 不过整体回滚(见 §6.5)
|
||||
4. 错误提示:条件自动生成人话 + 字段格式化(金额带 ¥、日期带格式)+ SQL 占位符填充
|
||||
5. 前端:删除先预览后确认、错误列表展示;模块管理「删除规则」tab(条件构造 + 复制到模块 + SQL 角落入口);系统管理全局规则列表页
|
||||
6. 二期:「已审核不能改」、Java 钩子、聚合展示
|
||||
5. 前端:关联面板一列化(去掉「有引用时拒绝删除」假列及其全部联动补丁)、规则面板收敛(去掉引用规则只读区与跳转)、列表删除先预览后确认、错误列表展示
|
||||
6. 二期:save.pre、Java 钩子、聚合展示
|
||||
|
||||
## 9. 定案记录
|
||||
|
||||
| # | 事项 | 结论 |
|
||||
|---|---|---|
|
||||
| 1 | 节奏 | 一期只做删除规则;表结构给保存校验留好位 |
|
||||
| 2 | 跨表判断 | 不做计算字段,直接写 SQL 兜底 |
|
||||
| 3 | SQL | 兜底逃生舱:不限制内容、不做体验优化、入口藏角落;**每次执行全程写日志** |
|
||||
| 4 | 旧 set-null | 废弃(财务上不该有悬空引用) |
|
||||
| 5 | 归档 | 通用列 `b_archived` + `b_archive_datetime` |
|
||||
| 6 | 条件规模 | 嵌套 ≤ 8 层、节点 ≤ 100;界面分组 ≤ 3 层 |
|
||||
| 7 | 错误类型 | 不单独存列,由规则写法决定 |
|
||||
| 8 | 级联清理 | 删模块/关系时业务层连带删规则(同权限点模式) |
|
||||
| 9 | 批量 | ids 不设上限;整批一个数组参数 |
|
||||
| 10 | 复制 | 规则可多选模块批量克隆;引用规则除外 |
|
||||
| 11 | saveobjt 集成 | 引擎拆检查器/执行器;saveobjt 的 deletes 分支同事务调检查器,不过则整批回滚(§6.5);前端入参不变 |
|
||||
| # | 事项 | 结论 | 版本 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 节奏 | 一期只做删除规则;保存校验二期再议,不留 `b_hook` 列 | v2 修订 |
|
||||
| 2 | 跨表判断 | 不做计算字段,直接写 SQL 兜底 | v1 |
|
||||
| 3 | SQL | 兜底逃生舱:不限制内容、不做体验优化、入口藏角落;**每次执行全程写日志** | v1 |
|
||||
| 4 | 引用保护 | **`s_relation.b_on_delete='restrict'`(默认)表达,不进 `s_rule`**;删关系时规则随之消失,无孤儿清理 | v2 修订 |
|
||||
| 5 | 归档 | 通用列 `b_archived` + `b_archive_datetime` | v1 |
|
||||
| 6 | 条件规模 | 嵌套 ≤ 8 层、节点 ≤ 100;界面分组 ≤ 3 层 | v1 |
|
||||
| 7 | 规则类型 | **显式 `b_kind` 列(condition / sql)**,不靠 `b_predicate` 首字符推断 | v2 修订 |
|
||||
| 8 | 级联清理 | 删模块时服务层连带删 `s_rule`(`b_module_id` 单条件直查);关系级规则不复存在 | v2 修订 |
|
||||
| 9 | 批量 | ids 不设上限;整批一个数组参数(OPENJSON) | v1 |
|
||||
| 10 | 复制 | **全部规则可多选模块批量克隆**(v2 起无引用规则例外);复制时校验目标模块存在条件所用字段 | v2 修订 |
|
||||
| 11 | saveobjt 集成 | 引擎拆检查器/执行器;saveobjt 的 deletes 分支同事务调检查器,不过则整批回滚(§6.5);前端入参不变 | v1 |
|
||||
| 12 | 方向语义 | 四值统一描述「删除被引用方时引用方怎么办」;被引用方由基数判定(§3) | v2 新增 |
|
||||
| 13 | 默认值 | `restrict`:被引用即不可删为默认,放开需显式改 `none` | v2 新增 |
|
||||
| 14 | 存量迁移 | 库中无数据,直接按新结构建表,不做转换 | v2 新增 |
|
||||
@@ -0,0 +1,73 @@
|
||||
# FMS 删除规则改造待做
|
||||
|
||||
> 创建:设计阶段已闭合、实施未开始。明日开工照此清单执行。
|
||||
> 设计依据:`FMS删除规则引擎设计.md` **v2.0**(权威实现规格)、`FMS模块设计评审.md` v1.2(决策记录)。
|
||||
> 前提确认:库中无存量规则/关系数据(`sql/` 种子脚本中 `s_relation` 零 insert),**全部按新结构直接建,不做迁移**。
|
||||
|
||||
## 已定案的设计决策(不需要再讨论)
|
||||
|
||||
| # | 决策 | 出处 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `s_relation.b_on_delete` 单列四值:`restrict`(默认)/ `cascade` / `archive` / `none`;语义统一为「删除**被引用方**时,**引用方**怎么办」;被引用方由基数判定(many_to_one=目标 / one_to_many=源 / one_to_one=源) | 删除规则 v2.0 §3 |
|
||||
| 2 | 引用保护退出 `s_rule`,由 `b_on_delete='restrict'`(默认)表达;「有引用时拒绝删除」假列及全部联动补丁删除 | 删除规则 v2.0 §3 |
|
||||
| 3 | `s_rule` 新结构:`b_module_id` 直存、显式 `b_kind`(condition/sql)、去掉 `b_scope_type`/`b_scope_id`/`b_hook` | 删除规则 v2.0 §6.2 |
|
||||
| 4 | 中间表模块挂业务域 `module` 节点下、与主表模块平级(规范已写入核心表结构设计 §4.3) | 评审 v1.2 问题四 |
|
||||
| 5 | 模块管理分节改为「数据行为 / 访问控制」,只改 `configGroups` 分组,面板/路由/数据不动;与删除规则前端改造同批实施 | 评审 v1.2 问题二 |
|
||||
|
||||
## 待做清单(按执行顺序)
|
||||
|
||||
### ① 重写 SQL 脚本 —— 对应设计 v2.0 §6.1 / §6.2
|
||||
|
||||
现状:两个脚本还是 v1 结构(`b_on_delete` 默认 `none` 三值、`s_rule` 带 `b_scope_type`/`b_scope_id`/`b_hook`)。
|
||||
|
||||
- [ ] `sql/fms_delete_rule.sql` 重写:
|
||||
- `s_relation.b_on_delete`:四值,**默认 `'restrict'`**(现脚本 L18 默认是 `'none'`)
|
||||
- `s_rule` 按新结构重建:`b_module_id varchar(50) not null`、`b_kind varchar(20) not null`(condition / sql)、`b_predicate not null`、`b_message`、`b_canuse`、`b_xh`、审计四列;去掉 `b_scope_type`/`b_scope_id`/`b_hook`
|
||||
- 索引:`ix_s_rule_module on s_rule (b_module_id, b_canuse, b_xh)`
|
||||
- 保留脚本头「可重复执行」说明与执行方式注释
|
||||
- [ ] `sql/fms_core.sql` 同步:L208 的 `b_on_delete` 注释与默认值、L228-256 的 `s_rule` 段整体替换(与 `fms_delete_rule.sql` 保持一致,注意两脚本职责分工——core 建全表、delete_rule 只做本特性增量,按现有模式来)
|
||||
- [ ] 建表 SQL 的 `b_archived` / `b_archive_datetime` 通用列暂不加——它是 `archive` 行为的**运行时前提**(引用方表需有此列),到后端实现时再决定加在哪些业务表
|
||||
|
||||
### ② 后端引擎 —— 对应设计 v2.0 §6.4 / §6.5 / §6.6
|
||||
|
||||
- [ ] 新建 `DeleteRuleChecker`(检查器):`check(connection, 模块, ids, 小闭包)`
|
||||
- 模块条件:清单内每个模块的 `s_rule`(`b_canuse=1`)逐条执行;condition → JSON AST 翻译参数化 SQL(AST 格式见 §6.3,嵌套 ≤8 层、节点 ≤100);sql → `:ids` 替换(OPENJSON 数组参数,绕开 2100 绑定变量上限),查出有结果即记错误,**全程写技术日志**(实际 SQL+参数+行数)
|
||||
- 引用保护:清单内每条记录查它作为被引用方的 `restrict` 入边(`b_on_delete='restrict'` 且 `b_canuse=1`),引用方存在且不在清单内 → 记错误(带引用方单号)
|
||||
- [ ] 新建 `DeleteRuleExecutor`(执行器):从 ids 出发沿 `b_on_delete ∈ {cascade, archive}` 的关系逐层展开删除清单(被删的是被引用方;已装过跳过防环;超 10 层报配置错误)
|
||||
- [ ] `DataController` 加 `POST /data/deleteobj`:`{ moduleCode, ids, dryRun }`;dryRun 返回「将删除 X + Y」摘要;真删开事务**重跑检查**(预览通过不算数)→ archive 打标记 → 先子后父物理删 → 审计日志
|
||||
- [ ] `DataSaveService.saveTable()` delete 分支接入(§6.5,**重要**):
|
||||
- 真删前按表名反查模块(`b_savetable`),查不到跳过(日志表等)
|
||||
- 同事务调检查器;**小闭包语义**:本次 saveobjt 所有 deletes 覆盖的 id 传给检查器,引用方也在被删之列不算外部引用
|
||||
- 不过 → 抛 BusinessException → 整个 saveobjt 回滚
|
||||
- [ ] 错误消息生成:condition 自动生成人话(带实际值 + 字段格式化:金额 ¥、日期格式);sql 规则用 `b_message` 的 `{列名}` 占位符填充;错误按规则归类、每类带数量和前几条示例
|
||||
|
||||
### ③ 前端改造 —— 对应设计 v2.0 §8 步骤 5 + 评审问题二
|
||||
|
||||
- [ ] `ModuleRelationPanel.vue`:删除相关从三栏位(`b_relation_type` / `b_on_delete` / `_hasRefRule`)收敛为 `b_relation_type` + `b_on_delete`(四值下拉,默认 restrict)
|
||||
- 删掉:`_hasRefRule` 假列、`setRefRule`、改关系主键时作废旧键规则及提示(L326-334)、填不全四列禁用 checkbox
|
||||
- `many_to_one` + `cascade` 不再界面禁用,改保存校验拦截(对齐「校验落后端」原则——若后端引擎做此校验,前端可只做提示)
|
||||
- [ ] `ModuleRulePanel.vue`:去掉引用规则只读区 + `navigate` 跳转;列表只剩模块级规则(condition / sql)
|
||||
- [ ] `ModuleRuleEditModal.vue` + `ruleUtils.js`:
|
||||
- `parseRulePredicate` 三态推断改两态(读 `b_kind`,不再看首字符 `{`)
|
||||
- 删掉 `relationScopeId` / `parseRelationScopeId` / `isCompleteRelationKey` / `REF_RULE_HINT` / `createRefRuleRow` / `RULE_SCOPE_RELATION`
|
||||
- `createRuleRow` 带 `b_kind`;新增默认 `condition`
|
||||
- [ ] `module-management/index.vue`:
|
||||
- 删 `refRules` / `refRules_org` / `refRulesReverse` 三组数据及加载、diff、清理逻辑(L875-898 关系级规则清理段改为只清模块级)
|
||||
- `configGroups` 分节调整:「数据行为 → 模块关联(含删除行为) / 删除规则 / 自动编码」「访问控制 → 权限」;section key 不动
|
||||
- [ ] `FmsModuleListPage.vue` `deleteSelected`(L877):直删改两步——先 `deleteobj dryRun=true`,有错误弹错误列表,无错误弹确认框(含连带摘要),确认后真删
|
||||
|
||||
### ④ 验收检查
|
||||
|
||||
- [ ] 全局搜索无旧导出残留:`relationScopeId`、`createRefRuleRow`、`REF_RULE_HINT`、`b_scope_type`、`b_scope_id`、`_hasRefRule`、`setRefRule`
|
||||
- [ ] `pnpm check`(lint + fmt + build)通过
|
||||
- [ ] 跑受影响模块的前端测试;`ruleUtils` / 面板相关 spec 按新契约更新(测试约定见 `fms-vue/tests/README.md`:文件同名、中文用例名)
|
||||
- [ ] 后端:`mvnw test`;引擎核心逻辑(检查器翻译、闭包语义、环检测)补测试
|
||||
- [ ] 手工链路验证:配一条「审核中不可删」→ 列表删被拦(人话提示)→ 表单里删行同样被拦(saveobjt 回滚)→ cascade 关系连带展开预览 → restrict 默认生效
|
||||
|
||||
## 注意事项(动工前再读一遍)
|
||||
|
||||
1. **`sql/fms_delete_rule.sql` 现有 L22 是 `drop table if exists s_rule`**——虽然确认无数据,重写后仍保留 drop+create 模式,但执行前先 `select count(*)` 确认一次
|
||||
2. 规则引擎是**当前最大缺口**:现在界面上配的删除规则完全不生效(后端 `s_rule` 相关零实现)。②完成后配置才有意义,①③可以和②并行
|
||||
3. 执行 SQL 用 `RunSqlFile`:`java -cp "tools/migration/mssql-jdbc-13.4.0.jre11.jar" tools/migration/RunSqlFile.java ../sql/fms_delete_rule.sql --dry-run` 先 dry-run
|
||||
4. 改前端遵守 `开发规范.md`:改前先读、最小修改、不为兼容旧名留转发别名(调用方可控,直接迁移删旧代码)
|
||||
5. `s_rule` 主键仍是雪花 `b_id`(前端 `nextIdApi` 取号,与业务表一致);**配置表不用 nextIdApi 的规则是「模块配置表用业务键」**——`s_rule` 无天然业务键,属例外(v2.0 §6.2 已注明)
|
||||
@@ -251,6 +251,7 @@ create index ix_s_module_save_table
|
||||
- 不允许模块指向自身或形成循环;`b_id` 创建后原则上不可修改;这些规则由业务层校验。
|
||||
- `b_depth`、`b_path` 与 `b_parent_id` 不一致时,以 `b_parent_id` 为准并重建冗余字段。
|
||||
- 数据之间的主子表、引用和多对多关系统一由 `s_relation` 表达。
|
||||
- 多对多的中间表模块(`data` 模块)在模块树中统一挂在所属业务域 `module` 节点下,与参与多对多的主表模块平级;不挂在任一参与多对多的主表模块下——模块树只表达业务组织,不表达数据关系,混入会污染 `b_path` 子树查询、菜单挂载与权限继承。
|
||||
- 模块级扩展配置使用 `b_config_json`,只保存低频、非核心的扩展属性,不重复保存字段定义、权限或界面布局。
|
||||
|
||||
### 4.4 查询来源优先级
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
# 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 边界。
|
||||
Reference in new issue
Block a user