263 lines
15 KiB
Markdown
263 lines
15 KiB
Markdown
# FMS 删除策略重构 — 待决策问题清单(已全部关闭)
|
||
|
||
> **状态:本文所列问题均已决策并实施完毕,保留作为决策过程的记录。**
|
||
> 结论已回写进 `FMS删除策略重构设计.md`(v1.1 / v1.2),代码已按结论落地。
|
||
> 阅读本文时请以设计文档为准;本文只说明"为什么设计长成现在这样"。
|
||
>
|
||
> 最终决策对照:
|
||
>
|
||
> | # | 问题 | 结论 |
|
||
> | --- | --- | --- |
|
||
> | 1 | `b_on_delete` 默认值 | 新建关系默认 `restrict`;**存量 `none` 不回填**,避免上线后突然改变存量删除行为(设计 §3.1) |
|
||
> | 2 | SQL 只读校验 | 复用 `DbUtils.validateReadOnlySql` + `DbUtils.loadDataBySql(Connection, String)`,不新建第二套判定器(设计 §4.4) |
|
||
> | 3 | `:ids` 参数形式 | 非 JDBC 命名参数;引擎按主键类型替换为**经过校验和转义**的字面量 `IN` 列表(设计 §4.2) |
|
||
> | 4 | 两条路径清单合并 | 显式删除与 cascade 展开按「表 + 主键字段 + 主键值」**去重合并**;同一记录同时以两种目标提交直接报冲突(设计 §6.2) |
|
||
> | 5 | `b_module_id` 类型 | `varchar(50)` 模块编码,与 `s_relation` 等配置表一致(设计 §4.1) |
|
||
> | 6 | 前端多对一硬编码 | `ModuleRelationPanel.vue` 的 `isManyToOne` 禁用/重置/警告与 `_hasRefRule` 联动全部删除(设计 §3.2、§9 第三阶段) |
|
||
> | 7 | 视图表回退 | 删除操作只允许 `b_save_table`;为空时前后端都拒绝,绝不回退 `b_view_table`(设计 §6.1) |
|
||
>
|
||
> 除此之外,设计文档还新增了两处本文未提出的必要补充:
|
||
> `target=module/direct` 二分(§1.1,避免给 `bf_files` 等技术表虚构模块)和文件删除路径(§6.3)。
|
||
|
||
---
|
||
|
||
> 用途(原始):本文把《FMS删除策略重构设计.md》落地前必须先拍板的问题集中列出,供多方(人工 + 其他 AI)评审。
|
||
> 阅读者可能无法访问本代码库,因此每个问题都附带了**必要的事实依据**(文件路径 + 行号 + 现有代码片段)。
|
||
>
|
||
> 背景:`FMS删除策略重构设计.md` v1.0 是一份"架构定稿、由 AI 执行"的文档。本文**不否定其架构方向**,
|
||
> 只列出执行前存在的现状误判、行为反转风险与设计空白。
|
||
>
|
||
> 已确认的前提:**当前数据库无配置数据,`s_rule` 可直接删表重建**(该前提消除了"数据迁移"类问题)。
|
||
|
||
---
|
||
|
||
## 一、先说结论
|
||
|
||
文档的**架构判断是正确的**,值得保留:
|
||
|
||
1. 两类配置不合并(关联处置 vs SQL 规则,依赖维度不同);
|
||
2. SQL 规则只保留"返回至少一行 → 拒绝"这一条契约,废弃 `b_scope_type` / `b_scope_id` / `b_hook` / `b_kind` / `b_predicate`;
|
||
3. 删除引擎三职责(计划器 / 检查器 / 执行器),对前端只暴露一个动作;
|
||
4. 列表删除与 `saveobjt` 删行共用同一套检查逻辑。
|
||
|
||
但文档 §9「AI 实施顺序」建立在**对代码现状的错误假设**上,直接照做会出错。
|
||
以下 4 个问题的性质不同,其中 **问题 1 和问题 3 必须人工拍板**,问题 2、4 属于文档补全。
|
||
|
||
---
|
||
|
||
## 二、问题 1(必须决策):`b_on_delete` 默认值 `none → restrict` 是行为反转
|
||
|
||
### 事实依据
|
||
|
||
现状建表语句 `sql/fms_core.sql:208`:
|
||
|
||
```sql
|
||
b_on_delete varchar(20) not null default 'none', -- 删除连带:none不连带 / cascade连带删除 / archive连带归档
|
||
```
|
||
|
||
文档 §3.1 要求:**默认值使用 `restrict`**,且只允许 `restrict` / `cascade` / `none` 三值。
|
||
|
||
### 为什么这是问题
|
||
|
||
`none` 与 `restrict` 的语义是相反的:
|
||
|
||
| 取值 | 语义 | 新增一条关系后的默认行为 |
|
||
| --- | --- | --- |
|
||
| `none`(现状默认) | 不展开、**不保护**,由配置者承担结果 | 删主单时明细不受任何检查 |
|
||
| `restrict`(文档要求) | 被引用方仍有外部引用时**拒绝删除** | 删主单时会被明细挡住 |
|
||
|
||
文档 §8 已明确"没有登记到 `s_relation` 的引用不在自动保护范围内",这没问题;
|
||
但**已登记关系且取值为 `none`** 的那些,默认值一改,行为立即翻转。
|
||
|
||
### 与现有前端代码的直接冲突
|
||
|
||
`fms-vue/src/views/module/module-management/ModuleRelationPanel.vue:317-318`:
|
||
|
||
```js
|
||
if (dataIndex === 'b_relation_type' && isManyToOne(row) && row.b_on_delete !== 'none') {
|
||
row.b_on_delete = 'none'
|
||
}
|
||
```
|
||
|
||
前端现在把 `many_to_one` 关系的 `b_on_delete` **强制重置为 `none`**。
|
||
而文档 §3.2 明确反对这一做法:"`many_to_one` 不在前端被硬编码禁止 `cascade`……是否允许级联由配置和业务语义决定"。
|
||
|
||
也就是说,**文档 §3.2 那一整节实际上是在推翻一段已存在的代码**,但文档没有点名,
|
||
执行者可能只改后端、漏掉这段前端硬编码,导致新配置在界面上被静默改回 `none`。
|
||
|
||
### 待决策
|
||
|
||
`s_relation.b_on_delete` 的默认值如何处理?可选:
|
||
|
||
- **方案 A(推荐)**:列默认值改为 `restrict`(新增关系默认受保护),同时写一条回填语句把**存量关系**置为 `none`,保证现有删除行为不变。
|
||
- **方案 B**:存量关系也一并改为 `restrict`,一致性最好,但上线后可能突然挡住一批原本能删的操作。
|
||
- **方案 C**:保持 `none` 为默认,仅补充 `cascade` / `restrict` 的显式配置能力(与文档 §3.1 不一致,需回改文档)。
|
||
|
||
**并且需要明确**:文档 §3.1 说的"默认值",指的是**数据库列默认值**,还是**配置界面新增行时的默认值**?两者可以不同。
|
||
|
||
---
|
||
|
||
## 三、问题 2(文档补全):SQL 规则的"只读"边界存在现成手段,但文档未引用
|
||
|
||
文档 §4.2 约定"SQL 必须是只读查询",§4.3 重申"配置 SQL 不等于允许 SQL 修改数据",
|
||
但**没有指定用什么机制保证**,也没说复用现有能力。
|
||
|
||
### 事实依据
|
||
|
||
- 项目规范《开发规范.md》第 8 条已把"只执行查询语句"定为系统级边界:
|
||
> **原生 SQL 仅限查询** — `b_query_sql`、查询 SQL 扩展和调用方传入的 SQL 可以是任意合法的 `SELECT` 语句……
|
||
> 禁止执行 `INSERT`、`UPDATE`、`DELETE`、DDL、存储过程调用及其他非查询语句。
|
||
- 现有实现样板:`fms-api/src/main/java/cn/g3soft/fmsapi/service/SqlPermissionService.java`
|
||
(模块级 SQL 条件拼装与服务端数据范围过滤,可直接参照其连接管理与查询执行方式)。
|
||
|
||
### 建议
|
||
|
||
在 §4.2 补一句:**复用现有 `SELECT` 校验入口与连接管理,不新写一套 SQL 判定器**
|
||
(符合《开发规范.md》第 3、4 条"复用现有模式 / 修改范围最小化")。
|
||
|
||
---
|
||
|
||
## 四、问题 3(必须决策):`:ids` 的参数形式未定义,而它决定契约能否落地
|
||
|
||
### 事实依据
|
||
|
||
文档 §4.2 给出的 SQL 契约示例:
|
||
|
||
```sql
|
||
select b_id, b_no
|
||
from cw_receipt
|
||
where b_id in (:ids)
|
||
and b_status in ('审核中', '已生效')
|
||
```
|
||
|
||
文档只写"统一使用 `:ids` 作为批量参数",**没有说明这个占位符在实现层是什么**。
|
||
|
||
而现有数据访问层的能力是**字面量拼接**,不是命名参数:
|
||
`DbUtils` 提供 `toSqlStringLiteral(...)`;`DataSaveService.executeRows(...)`
|
||
(`fms-api/.../service/DataSaveService.java:460-479`)逐行调用 `dbUtils.delete/update/insert`,
|
||
`SqlPermissionService` 拼数据范围条件时同样是 `dbUtils.toSqlStringLiteral(userId)` 直接拼进 SQL 文本。
|
||
|
||
### 因此 `:ids` 只能有三种落地方式,必须选一个
|
||
|
||
- **方案 A**:引擎把 `:ids` 替换为拼接好的 `IN (...)` 列表(雪花 bigint 或业务键 varchar,按类型加引号/校验)。与现有代码风格一致。
|
||
- **方案 B**:改造 `DbUtils` 支持真正的参数化(`?` / 命名参数),改动面大,触及所有保存/查询路径。
|
||
- **方案 C**:把待删 ID 集先写入临时表,SQL 用 `join` 临时表(避免超长 `IN` 列表,但引入临时表生命周期管理)。
|
||
|
||
**相关约束**:《开发规范.md》第 5 条"当前系统按内部 ERP 处理……不额外引入复杂的安全防护层",
|
||
第 6 条"SQL 驱动优先"。按此基调,**方案 A 更契合**,但需要文档显式写明,
|
||
并要求删除引擎统一对 `ids` 做**类型校验**(不能把用户输入直接拼进 SQL)。
|
||
|
||
此外还需明确:`:ids` 是**当前模块本次待删 ID**,还是**整个删除清单所有模块的 ID**?
|
||
文档 §4.2 写"SQL 接收当前模块本次待删除的 ID 集合",暗示是前者,
|
||
但多条规则跨模块级联时,配置者可能期望看到子表 ID —— 这一点容易产生歧义。
|
||
|
||
---
|
||
|
||
## 五、问题 4(必须是设计决策,不是实现细节):两条路径的待删清单如何合并
|
||
|
||
文档 §6 说列表删除和 `saveobjt` 删行"共用一套检查逻辑",方向正确,但**合并规则是空白的**。
|
||
|
||
### 事实依据
|
||
|
||
`saveobjt` 的实际执行顺序(`fms-api/.../service/DataSaveService.java:417-437`):
|
||
|
||
```java
|
||
executeRows(connection, table, keyColumns, "delete", rows(request, "deletes"));
|
||
executeRows(connection, table, keyColumns, "update", rows(request, "updates"));
|
||
executeRows(connection, table, keyColumns, "insert", rows(request, "inserts"));
|
||
```
|
||
|
||
即 **先 delete → 再 update → 最后 insert**,且逐行循环。
|
||
|
||
前端列表删除现状(`fms-vue/src/components/fms-module-list/FmsModuleListPage.vue:877-933`):
|
||
`deleteSelected()` 取 `resolveSaveTable()`(`:754`,即 `b_save_table || b_view_table`),
|
||
然后调用 `saveObjectApi([{ table, key_field, deletes }])` —— **传的是裸表名,不是模块编码**。
|
||
|
||
### 由此产生的三个未答问题
|
||
|
||
1. **`moduleCode + ids` 如何反查表和主键?**
|
||
文档 §6.1 要求列表删除改为传 `moduleCode + ids`,但现有链路是 `table + key_field`。
|
||
后端需要新增"模块 → 保存表 + 主键字段"的反查,且需考虑一个模块可能对应多张表的情况。
|
||
`saveobjt` 路径则保留 `table` 入参 —— **两条路径的入参格式不一致,如何落到同一个服务?**
|
||
|
||
2. **cascade 展开的子记录与请求自带的 `deletes` 行如何合并去重?**
|
||
例如 `saveobjt` 请求本身带了明细行的 `deletes`(表单删行),而 cascade 又从主单展开出同一批明细。
|
||
文档 §5.2 只说"删除清单内的引用方视为本次一起删除,不触发外部引用保护",
|
||
但**没说这份清单怎么构建、两处来源怎么去重**。若不去重,可能出现同一记录删两次(第二次影响 0 行,或触发异常)。
|
||
|
||
3. **cascade 展开的记录插入 `delete → update → insert` 的哪个位置?**
|
||
若模块级删除规则或外键约束要求"子先父后",展开出的多层级记录必须排在请求自带 `deletes` 的**合适位置**。
|
||
文档 §5.3 给的流程是"生成清单 → 检查 → 子记录到主记录物理删除",但**没有说明它与 `saveobjt` 既有顺序约束的关系**。
|
||
|
||
### 待决策
|
||
|
||
- **方案 A(推荐)**:cascade 展开结果与 `saveobjt` 自带 `deletes` 按「模块 + 主键」**去重合并成一份清单**,再统一检查、按子先父后排序后删除。
|
||
- **方案 B**:`saveobjt` 路径不做 cascade,只对请求自带的 `deletes` 跑规则检查(简单,但两条路径语义不再等价,与文档 §6 目标冲突)。
|
||
|
||
---
|
||
|
||
## 六、问题 5(文档补全):`s_delete_rule.b_module_id` 的类型与取号方式
|
||
|
||
文档 §4.1 概念字段表列出 `b_id`(雪花主键)与 `b_module_id`(规则所属数据模块),
|
||
但**没有给出 `b_module_id` 的类型**,也没有说明新增规则时是否走前端取号。
|
||
|
||
### 事实依据
|
||
|
||
《开发规范.md》「面板式配置页(左树右面板)设计」第 6 条:
|
||
|
||
> **模块配置不使用雪花临时 ID** — 模块管理中的 `s_module` 使用业务编码,
|
||
> `s_field`、`s_module_schema`、`s_autocode`、`s_relation` 等配置表使用业务键或联合主键;
|
||
> 新增配置行直接使用业务字段组成的临时唯一键,不调用 `nextIdApi`,保存时也不做"临时 ID → 雪花 ID"转换。
|
||
|
||
即模块配置类的表普遍用**业务编码 varchar(50)** 作为模块引用(`s_relation.b_source_module_id` 即如此)。
|
||
而 `s_delete_rule` 与 `s_rule` 同族,旧表用的是 `b_scope_id varchar(250)`。
|
||
|
||
### 待决策
|
||
|
||
- `b_module_id` 用 `varchar(50)` 业务编码(与 `s_relation` 一致,前端不做 ID 换号),还是 `bigint` 雪花 ID?
|
||
- 若用 `varchar(50)`:`b_id` 是规则主键(雪花),新增规则仍需前端取号 —— 这与"配置表不做 ID 转换"是否冲突?
|
||
旧 `s_rule` 的做法是"前端经 `/data/nextid` 取号"(见 `sql/fms_delete_rule.sql:29` 注释),文档未表态。
|
||
|
||
---
|
||
|
||
## 七、问题 6(次要):文档对"现状"的描述性错误
|
||
|
||
这类问题不影响架构,但会让执行者误判起点,建议一并修正:
|
||
|
||
| 文档位置 | 文档表述 | 实际情况 |
|
||
| --- | --- | --- |
|
||
| §1 | `s_relation.b_on_delete` | **已存在**(`sql/fms_core.sql:208`),无需"增加" |
|
||
| §3.1 | "并增加或保留一个删除行为列" | 列已存在,只需改默认值语义 |
|
||
| §9 第一阶段 2 | "删除旧 `s_rule` 结构,建立 `s_delete_rule`" | 旧设计**已完整落地**:`sql/fms_delete_rule.sql` 建了 `s_rule`,且前端整套已实现 |
|
||
| §7 | 待删除的旧概念 `勾条件` / `引用规则` / `refRules` / `relationScopeId` / `b_scope_type` … | **全部真实存在**,不是待设计的抽象概念(见下方清单) |
|
||
|
||
### 旧设计在前端的实际落点(§7 要求删除的对象)
|
||
|
||
- `fms-vue/src/views/module/module-management/ruleUtils.js`(195 行,规则类型三态推断、关系业务键拼装)
|
||
- `fms-vue/src/views/module/module-management/ModuleRulePanel.vue`
|
||
- `fms-vue/src/views/module/module-management/ModuleRuleEditModal.vue`
|
||
- `fms-vue/src/views/module/module-management/ModuleRelationPanel.vue`
|
||
- `fms-vue/src/views/module/module-management/index.vue`(`rules` / `refRules` / `refRulesReverse` / `refRules_org` 等状态与读写逻辑)
|
||
- `sql/fms_core.sql:237-256`(`s_rule` 建表 + 索引)
|
||
- `sql/fms_delete_rule.sql`(旧迁移脚本)
|
||
|
||
值得注意:`ruleUtils.js:62-74` 的 `parseRulePredicate()` 用
|
||
"是不是 `{` 开头 / 能不能 `JSON.parse` / `kind === 'dsl'`"来**推断规则类型**(引用 / 勾条件 / SQL 三态)。
|
||
这种靠内容猜测类型的做法脆弱且难维护,正是新版"只保留 SQL + 关系处置"的改进理由 ——
|
||
**建议把这条写进文档 §4.3,作为废弃 `s_rule` 的具体论据**,比现在的抽象论证更有说服力。
|
||
|
||
---
|
||
|
||
## 八、汇总:需要人工拍板的决策点
|
||
|
||
| # | 决策点 | 影响 |
|
||
| --- | --- | --- |
|
||
| 1 | `b_on_delete` 默认值:存量是否升级 `restrict`;"默认"指列还是界面 | 决定上线后现有删除行为是否变化 |
|
||
| 2 | `:ids` 落地方式:拼接 IN / 参数化 / 临时表;以及 `ids` 范围 | 决定 SQL 契约能否实现及引擎改造量 |
|
||
| 3 | 两条路径清单合并:是否去重合并;cascade 是否参与 `saveobjt` | 决定两条路径语义是否真正等价 |
|
||
| 4 | `b_module_id` 类型与规则取号方式 | 决定建表脚本与前端取号逻辑 |
|
||
| 5 | `ModuleRelationPanel.vue:317-318` 的 `many_to_one → none` 硬编码是否删除 | 不删则新配置在界面被静默改回 |
|
||
|
||
其余(问题 2、6)为文档补全,不需决策,但**建议在开工前先改文档**,
|
||
否则执行者会按错误的现状描述行动。
|