Files
workspace/code/fms/FMS模块设计评审.md
T
2026-09-19 21:17:02 +08:00

14 KiB
Raw Blame History

FMS 模块设计评审

版本:v1.2。评审对象:FMS新系统核心表结构设计.md(模块 / 字段 / 界面配置 / 模块关系 / 权限)。 关联:FMS删除策略重构设计.md(当前删除策略架构)、开发规范.md、CODEBUDDY.md。 一句话:骨架是对的,改三处即可——关系删除行为一列化、配置分节按业务问题分、规则表达对齐权限体系。

v1.1 修订:问题一原建议「加 b_relation_kind 语义列 + 按语义推导默认行为」,定案为不加语义列、单列 b_on_delete 表达删除行为。该部分已由当前删除策略重构重新收敛为三值方案。

v1.2 修订:第八节第 3、4 项定案——中间表模块统一挂业务域节点下(规范已写入 FMS新系统核心表结构设计.md §4.3);模块管理分节调整随删除规则前端改造同一批实施。

当前实施以 FMS删除策略重构设计.md 为准。本评审中关于四值删除行为、归档、勾条件、引用规则和 s_rule 的章节保留为历史讨论,不再作为实施规格。


一、总评

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

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

数据结构 → 字段定义
界面配置 → 表单设计 / 列表配置 / 查询配置
业务能力 → 自动编码 / 权限 / 模块关联 / 删除规则
高级设置 → 多语言

这是按「配置的载体」分,而不是按「业务问题」分。于是同一个业务问题被切到多处:

  • 「这个模块的记录在什么情况下不许删」→ 删除规则
  • 「这条记录被别人引用会怎样」→ 模块关联
  • 「谁能用、能看哪些字段、能看哪些数据」→ 权限

而用户在真实场景里问的是一个整体问题:「这张单据在什么情况下会被拦下来?」 答案分散在三个面板里。

这正是「删除规则要在模块关联和删除规则两处维护」困惑的根因——不是删除规则本身的问题,是分节维度的问题。

建议

按**「改这个模块会影响什么」**重新分节:

基础     → 模块定义(类型、查询来源、保存目标、主键)
字段     → 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 取代。


五、问题四:模块树与数据关系边界有摩擦

现状

§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) 后端零实现:删除计划、SQL 规则检查、deleteobj 和 b_on_delete 执行链路仍待实现
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 的架构调整;
  2. 模块管理分节改为「数据行为 / 访问控制」,按业务问题而非载体划分;删除策略统一为关联删除处理 + 删除 SQL 规则;
  3. 删除规则专用化:使用 s_delete_rule,只保留模块、SQL、提示、启用和顺序;
  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 边界。