15 KiB
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删除策略重构设计.mdv1.0 是一份"架构定稿、由 AI 执行"的文档。本文不否定其架构方向, 只列出执行前存在的现状误判、行为反转风险与设计空白。已确认的前提:当前数据库无配置数据,
s_rule可直接删表重建(该前提消除了"数据迁移"类问题)。
一、先说结论
文档的架构判断是正确的,值得保留:
- 两类配置不合并(关联处置 vs SQL 规则,依赖维度不同);
- SQL 规则只保留"返回至少一行 → 拒绝"这一条契约,废弃
b_scope_type/b_scope_id/b_hook/b_kind/b_predicate; - 删除引擎三职责(计划器 / 检查器 / 执行器),对前端只暴露一个动作;
- 列表删除与
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 }]) —— 传的是裸表名,不是模块编码。
由此产生的三个未答问题
-
moduleCode + ids如何反查表和主键? 文档 §6.1 要求列表删除改为传moduleCode + ids,但现有链路是table + key_field。 后端需要新增"模块 → 保存表 + 主键字段"的反查,且需考虑一个模块可能对应多张表的情况。saveobjt路径则保留table入参 —— 两条路径的入参格式不一致,如何落到同一个服务? -
cascade 展开的子记录与请求自带的
deletes行如何合并去重? 例如saveobjt请求本身带了明细行的deletes(表单删行),而 cascade 又从主单展开出同一批明细。 文档 §5.2 只说"删除清单内的引用方视为本次一起删除,不触发外部引用保护", 但没说这份清单怎么构建、两处来源怎么去重。若不去重,可能出现同一记录删两次(第二次影响 0 行,或触发异常)。 -
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.vuefms-vue/src/views/module/module-management/ModuleRuleEditModal.vuefms-vue/src/views/module/module-management/ModuleRelationPanel.vuefms-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)为文档补全,不需决策,但建议在开工前先改文档, 否则执行者会按错误的现状描述行动。