# 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)为文档补全,不需决策,但**建议在开工前先改文档**, 否则执行者会按错误的现状描述行动。