Files
workspace/code/fms/FMS删除策略重构-待决策问题清单.md
T
2026-09-19 21:17:02 +08:00

15 KiB
Raw Blame History

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:

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:

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 契约示例:

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):

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