diff --git a/code/fms/FMS删除策略重构-待决策问题清单.md b/code/fms/FMS删除策略重构-待决策问题清单.md deleted file mode 100644 index 9d5b4b02..00000000 --- a/code/fms/FMS删除策略重构-待决策问题清单.md +++ /dev/null @@ -1,262 +0,0 @@ -# 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)为文档补全,不需决策,但**建议在开工前先改文档**, -否则执行者会按错误的现状描述行动。 diff --git a/code/fms/FMS删除策略重构设计.md b/code/fms/FMS删除策略重构设计.md deleted file mode 100644 index fe731a40..00000000 --- a/code/fms/FMS删除策略重构设计.md +++ /dev/null @@ -1,402 +0,0 @@ -# FMS 删除策略重构设计 - -> 版本:v1.2 -> -> 本文取代此前的删除规则设计和改造清单。本文只确定架构、边界和实施思路,具体代码由 AI 按本文执行。 -> -> 设计目标:操作简单、执行逻辑简单、业务控制以 SQL 为主;第一期只做物理删除,不做归档。 - -## 1. 重构结论 - -删除能力保留两个来源,但统一放到一个“删除策略”入口下: - -```text -删除策略 -├── 关联删除处理 -│ └── s_relation.b_on_delete -└── 删除 SQL 规则 - └── s_delete_rule -``` - -两部分回答的问题不同: - -| 配置 | 要回答的问题 | 作用 | -| --- | --- | --- | -| 关联删除处理 | 删除一条记录时,关联记录怎么办 | 展开级联删除、保护外部引用 | -| 删除 SQL 规则 | 什么情况下不允许删除 | 检查状态、业务单据、跨表条件 | - -不把两类配置合成一张表。关联处理依赖关系方向和基数,SQL 规则依赖模块和删除批次;合成后会重新引入范围类型、拼接关系键和大量无意义的空字段。 - -### 1.1 删除对象不强制绑定模块 - -删除对象分为两类,不能为了统一接口给没有业务模块的表虚构 `moduleCode`: - -| 删除对象 | 调用参数 | 处理方式 | -| --- | --- | --- | -| 业务模块数据 | `target=module` + `moduleCode + ids` | 读取模块的保存表和主键,展开 `cascade`,执行 `restrict` 和模块 SQL 规则,再物理删除 | -| 技术表或专用资源 | `target=direct` + `table + key_field + ids` | 直接进入物理删除执行器,不读取模块关系,不执行模块 SQL 规则 | - -两类请求共用连接、事务、主键类型校验、批量上限、日志和底层物理删除执行器;只有第一类进入删除计划器和删除检查器。后端不根据表名反查模块,也不要求 `bf_files`、配置表、日志表等技术表登记一个无意义的模块。 - -“直接删除”表示删除策略层不再展开业务关系。它不表示可以绕过文件服务自身的清理步骤:文件删除仍由文件服务先删除本地文件或 OSS 对象,再删除 `bf_files` 行。 - -直接表目标只由文件服务或其他受控的后端服务调用,普通模块列表不允许让用户任意传入表名和主键字段。 - -## 2. 第一阶段范围 - -### 2.1 第一阶段包含 - -- 模块关系上的物理删除策略:`restrict`、`cascade`、`none`; -- 模块级 SQL 删除规则; -- 无模块技术表的直接物理删除; -- 模块列表删除和 `saveobjt` 中的模块删除共用一套检查逻辑;无模块删除共用同一个物理删除执行器; -- 批量删除、事务回滚、子记录先删; -- 删除失败时返回可读的规则或关系错误; -- 技术日志记录规则执行和删除结果。 - -### 2.2 第一阶段不包含 - -- `archive` 归档行为; -- `b_archived`、`b_archive_datetime` 等归档字段; -- JSON 条件 AST 和“勾条件”编辑器; -- 保存前规则、修改限制、Java 钩子和自动业务动作; -- 让配置 SQL 直接修改或删除业务数据; -- 为删除策略另建角色、模板或继承体系。 - -后续增加归档时,只扩展关联删除策略和执行器,不改变 SQL 删除规则的语义。 - -## 3. 关联删除处理 - -### 3.1 `s_relation.b_on_delete` - -`s_relation` 继续负责模块字段之间的关系映射,并增加或保留一个删除行为列。第一阶段只允许三个值: - -| 值 | 语义 | -| --- | --- | -| `restrict` | 被引用方仍有外部引用时拒绝删除 | -| `cascade` | 引用方记录加入本次物理删除清单 | -| `none` | 不自动处理关联记录 | - -新建关系的默认值使用 `restrict`。已存在的 `none` 不批量回填为 `restrict`,以免上线后突然改变存量删除行为;存量关系是否升级由单独迁移决定。只有登记在 `s_relation` 中的关系才进入自动保护范围;没有登记的表不视为系统已知引用。 - -### 3.2 关系方向 - -关系类型表达结构,删除行为表达处置方式,两者不互相推断: - -| 关系类型 | 外键所在侧 | 被引用方 | -| --- | --- | --- | -| `many_to_one` | 源字段 | 目标模块 | -| `one_to_many` | 目标字段 | 源模块 | -| `one_to_one` | 按系统约定 | 源模块 | - -`many_to_one` 不在前端被硬编码禁止 `cascade`。例如“明细 → 主单”的多对一关系,删除主单时级联删除明细是合理场景。是否允许级联由配置和业务语义决定,后端负责检查环路、重复记录和删除层级。 - -这是对现有前端行为的明确改造要求,不能只改后端: - -- 删除 `fms-vue/src/views/module/module-management/ModuleRelationPanel.vue` 中 `b_on_delete` 编辑器的 `disabled: (row) => isManyToOne(row)`; -- 删除同文件 `onCellChange` 中“关系类型切为多对一就把 `b_on_delete` 重置为 `none` 并弹出‘多对一关系不支持连带删除/连带归档’”的代码块; -- 删除 `_hasRefRule`、`setRefRule` 及其关系键变化联动,引用保护统一由 `b_on_delete = restrict` 表达; -- `b_on_delete` 的唯一配置来源是删除策略中的关联删除处理,不能再由关系类型或另一套勾选规则覆盖。 - -### 3.3 关联策略的边界 - -- `cascade` 只负责沿已登记关系展开删除清单,不负责判断业务状态; -- `restrict` 只检查删除清单之外的外部引用;同一批次内也被删除的引用不算外部引用; -- `none` 不展开、不保护,由配置者明确承担结果; -- 复杂引用判断使用删除 SQL 规则,不在关系表中增加第二套规则字段。 - -## 4. 删除 SQL 规则 - -### 4.1 表职责 - -第一阶段建议使用专用表 `s_delete_rule`,不再使用泛化的 `s_rule`。这样表名直接说明用途,也避免为未来不存在的规则类型预留抽象。 - -概念字段如下: - -| 字段 | 含义 | -| --- | --- | -| `b_id` | 规则主键,应用层生成的雪花 ID | -| `b_module_id` | 规则所属数据模块编码,`varchar(50)`;只允许模块删除使用 | -| `b_sql` | 只读查询 SQL | -| `b_message` | 删除失败时的提示文案 | -| `b_canuse` | 是否启用 | -| `b_xh` | 执行顺序 | -| `b_created_*` / `b_updated_*` | 审计字段 | - -不保留以下字段: - -```text -b_scope_type -b_scope_id -b_hook -b_kind -b_predicate -``` - -`s_delete_rule` 不覆盖无模块的直接表删除。技术表如果需要业务保护,由拥有该表的专用服务在调用删除前执行自己的检查;第一阶段不把表名、服务名和模块编码再抽象成一套通用作用域。 - -### 4.2 SQL 契约 - -模块删除规则 SQL 只表达“哪些记录不允许删除”: - -```text -返回至少一行 → 规则不通过 -返回零行 → 规则通过 -``` - -SQL 接收当前模块本次待删除的 ID 集合,统一使用 `:ids` 作为模板标记。它不是 JDBC 命名参数,执行前由删除引擎根据模块主键类型替换为经过校验和转义的字面量 `IN` 列表;第一阶段不引入表值参数或 `OPENJSON`。配置者可以自由使用本模块字段、其他表、聚合、子查询和连接条件。 - -示例语义: - -```sql -select b_id, b_no -from cw_receipt -where b_id in (:ids) - and b_status in ('审核中', '已生效') -``` - -约定: - -- SQL 必须是只读查询; -- SQL 必须限制在本次 `:ids` 对应的数据范围内; -- 单次删除 ID 数量设置明确上限,超限直接拒绝,避免生成过大的 SQL; -- 查询返回的行用于错误样例和日志,不直接执行 SQL 返回的修改意图; -- `b_message` 第一阶段使用固定文案,不做复杂占位符替换; -- 多条规则全部属于“禁止条件”,任意一条命中即拒绝整批删除; -- `b_xh` 只影响执行顺序和错误展示,不改变规则的并集语义。 - -### 4.3 为什么只保留 SQL - -- 不需要维护字段、操作符、常量、分组和 AST 转换; -- 跨表、聚合和历史业务条件可以直接表达; -- 后端只需要实现一个 SQL 执行契约; -- 前端只需要一个 SQL 编辑器和提示文案输入; -- 未来需要更强的条件能力时,仍可在 SQL 层扩展,不影响表结构。 - -配置 SQL 不等于允许 SQL 修改数据。删除动作仍由删除引擎统一完成,避免规则之间互相修改数据或破坏事务顺序。 - -### 4.4 只读校验和执行入口 - -删除规则不新建一套 SQL 安全判定器,直接复用现有 `DbUtils`: - -- `DbUtils.validateReadOnlySql(String)`:校验 SQL 以 `SELECT` / `WITH` 开头,并拒绝分号、注释和写入关键字; -- `DbUtils.loadDataBySql(Connection, String)`:复用 `saveobjt` 当前事务连接执行查询,并再次走同一只读校验; -- 删除引擎在替换 `:ids` 后调用上述入口,返回行即视为规则命中。 - -`SqlPermissionService` 负责动作权限、字段权限和数据范围条件,不作为删除规则 SQL 的只读校验器。若将来需要增强 SQL 解析能力,先扩展 `DbUtils` 的公共入口,不在删除引擎中复制一套正则或解析逻辑。 - -## 5. 删除引擎 - -删除引擎分为三个职责,但对前端暴露一个删除动作: - -```text -删除服务 -├── 删除计划器:仅对模块目标展开 cascade,形成模块和记录清单 -├── 删除检查器:仅对模块目标执行 s_delete_rule 和 restrict 检查 -└── 删除执行器:子记录优先,物理删除,提交审计日志 -``` - -直接表目标不经过前两个组件,经过同一个删除执行器。这样既不漏掉文件、配置表等非模块删除,也不会让直接删除意外触发某个表名猜出来的模块规则。 - -### 5.1 删除计划 - -从用户指定的模块和 ID 开始,沿启用的 `cascade` 关系递归展开: - -- 同一模块、同一主键只进入清单一次; -- 关系图出现环路时返回配置错误; -- 设置最大展开深度,防止错误配置造成无限递归; -- 记录每个模块将删除的数量,供日志和后续预览使用。 - -### 5.2 删除检查 - -对完整删除清单执行: - -1. 对每个模块运行启用的 `s_delete_rule`; -2. 对每条 `restrict` 入边检查外部引用; -3. 任何规则或关系失败,整批拒绝; -4. 错误按“模块 SQL 规则 / 外部引用关系”分类返回。 - -删除清单内的引用方视为本次一起删除,不触发外部引用保护。这是主单、明细和中间表一起删除时的必要语义。 - -直接表目标没有模块删除清单,因此不执行上述两类检查;它只返回数据库删除错误或专用服务的清理错误。 - -### 5.3 删除执行 - -第一阶段不做独立的“预览后确认”两阶段操作。前端可以先弹一次普通确认框,后端在同一个事务中完成计划、检查和删除: - -```text -开启事务 - → 生成删除清单 - → 执行 SQL 规则 - → 执行 restrict 检查 - → 子记录到主记录物理删除 - → 写审计日志 -提交事务 -``` - -任一步骤失败都回滚整批。以后需要删除预览时增加 `dryRun` 调用即可,不能改变正式删除流程。 - -直接表删除省略“生成删除清单 / SQL 规则 / restrict”三步,但仍在同一事务中执行数据库删除和审计。文件服务的外部对象操作不属于数据库事务:对象删除失败时不删数据库行;数据库删除失败时保留可重试或补偿的清理记录。 - -## 6. 调用路径 - -### 6.1 模块列表删除 - -列表删除通过统一删除服务处理: - -```text -前端传 target=module、moduleCode + ids -→ 删除计划器 -→ 删除检查器 -→ 删除执行器 -``` - -前端不再直接为删除拼装 `saveobjt` 的 `deletes` 行。 - -列表页解析写入目标时只读取 `b_save_table`。现有 `resolveSaveTable()` 中的 `b_save_table || b_view_table` 回退必须删除;`b_save_table` 为空时,所有保存、排序和删除写操作都提示“该模块未配置保存表,无法写入”,后端也必须再次拒绝,不能尝试对视图表执行写入。 - -### 6.2 `saveobjt` 中删除明细 - -`saveobjt` 的 `deletes` 分支仍保留原入参格式,但增加一个明确的删除目标类型: - -- `target=module`:必须提供 `moduleCode`;后端读取 `s_module.b_save_table` 和单一 `b_key_field`,调用同一删除计划器;`b_save_table` 为空时直接拒绝并返回“模块未配置保存表,无法删除”,不得使用 `b_view_table`;请求携带的 `table` / `key_field` 只能用于兼容和一致性校验; -- `target=direct`:必须提供 `table` / `key_field`,直接进入物理删除执行器,不做表名到模块的反查; -- 不允许以“有没有传 `moduleCode`”作为隐式分支。新接口必须明确目标类型,避免调用方漏传模块编码时静默绕过模块规则。 - -两种目标都: - -- 使用 `saveobjt` 当前事务连接; -- 收集本次请求中所有待删除记录;模块目标再展开 `cascade`,直接表目标不展开关系; -- 模块目标的同批次内引用不算外部引用; -- 显式删除和 `cascade` 展开的同一记录按解析后的“表 + 主键字段 + 主键值”去重;同一物理记录同时以模块目标和直接目标提交时直接报冲突,不执行两次; -- 任一检查失败,主表更新、明细删除和其他变更整体回滚。 - -这样列表删除和表单删行不会形成两套规则。 - -依赖行删除由调用方在请求里声明,服务端不按表名猜: - -```json -{ - "table": "s_power", - "key_field": "b_object_type,b_object_id", - "deletes": [{ "b_object_type": "module", "b_object_id": "sea" }], - "dependent_deletes": [ - { "table": "s_user_power", "key_field": "b_power_id", "source_field": "b_id" } - ] -} -``` - -`dependent_deletes` 的每一项是 `{table, key_field, source_field}`:对本表本次要删的每一行,用 `source_field` 的取值去删 `table` 中 `key_field` 等于该值的行;取值由后端按本行的列值条件查出(与物理删除的 `WHERE` 同口径),调用方不需要先把键值查出来再拼删除行。依赖行先于父行删除,与 `cascade` 同一执行顺序。 - -它解决“非模块表之间的引用清理”(例如权限点 `s_power` → 用户授权行 `s_user_power`,设计文档 §14.6):这类关系不登记在 `s_relation`(那是有模块归属的业务关系),也不能由服务端按表名反查——表名只出现在请求里,服务端不认识具体业务表。 - -### 6.3 文件和其他无模块删除 - -文件删除继续由 `/file/delete` 负责:按文件主键定位记录,先删除本地文件或 OSS 对象,再以 `target=direct`、`table=bf_files`、`key_field=subid` 删除数据库行。即使 `bf_files.mx_moduleid` 保存了业务模块编码,它也只是文件元数据,不是本次删除策略的模块目标;文件删除不需要 `moduleCode`,也不触发 `s_relation` 或 `s_delete_rule`。 - -如果某个业务明确要求“宿主单据处于某状态时不能删附件”,由文件服务显式调用宿主模块的 SQL 检查;直接表目标本身不自动反查 `father + mx_moduleid`。 - -其他没有业务模块的表沿用同样的直接表目标。若一张技术表存在自己的依赖清理,清理由该表所属服务显式完成;删除策略引擎不根据表名猜测级联关系。 - -## 7. 前端配置入口 - -模块管理的业务能力建议调整为: - -```text -数据行为 -├── 删除策略 -│ ├── 关联删除处理 -│ └── 删除 SQL 规则 -└── 自动编码 -``` - -“模块关联”可以继续作为关系字段映射的维护面板,但删除行为只能有一个编辑来源。若删除策略页面展示关联行,则模块关联页面中的删除行为列改为只读或跳转。 - -删除 SQL 规则面板只需要: - -- 新增、编辑、删除、启用/停用; -- SQL 文本编辑器; -- 固定提示文案; -- 执行顺序; -- 最近修改信息。 - -删除以下旧概念和前端联动: - -```text -勾条件 -引用规则 -refRules -relationScopeId -b_scope_type -b_scope_id -b_hook -``` - -同时必须删除现有实现中的以下联动,否则后端允许配置 `cascade` 也不会生效: - -```text -ModuleRelationPanel.vue:b_on_delete 编辑器的 isManyToOne disabled -ModuleRelationPanel.vue:onCellChange 中多对一重置 none 的分支和 Message.warning -ModuleRelationPanel.vue:_hasRefRule、setRefRule 及关系键变更时清理引用规则的逻辑 -FmsModuleListPage.vue:resolveSaveTable() 对 b_view_table 的删除回退 -``` - -`b_on_delete` 列可以继续显示在模块关联维护面板,但只能作为删除策略的同一份数据来源;不能同时保留一套只读/勾选/自动重置逻辑。 - -## 8. 错误、日志和权限 - -- SQL 规则命中时返回规则文案、所属模块、命中数量和少量样例 ID; -- `restrict` 命中时返回引用模块、引用字段和样例引用记录; -- 批量删除只返回聚合后的错误,避免逐行刷屏; -- 规则的新增、修改、删除沿用模块配置权限; -- SQL 文本、参数、命中行数和请求标识写入技术日志; -- 直接表删除记录表名、主键字段、数量和请求来源;按明确的目标类型标记为 `direct`,便于审计和排查; -- 规则停用使用 `b_canuse = 0`,删除规则本身才做物理删除并记录审计; -- 没有登记到 `s_relation` 的引用不在自动保护范围内,这属于元数据配置边界。 - -## 9. AI 实施顺序 - -### 第一阶段:元数据和脚本 - -1. `s_relation` 确认 `b_on_delete` 三值和默认 `restrict`; -2. 删除旧 `s_rule` 结构,建立 `s_delete_rule`; -3. 同步核心建表脚本和增量脚本; -4. 不加入任何归档字段。 - -### 第二阶段:后端 - -1. 实现删除计划器; -2. 实现 SQL 规则检查器,复用 `DbUtils.validateReadOnlySql` 和 `DbUtils.loadDataBySql(Connection, String)`,不新建第二套只读判定器; -3. 实现 `restrict` 外部引用检查; -4. 实现子记录优先的物理删除执行器; -5. 定义 `target=module/direct` 两种删除目标,接入列表删除入口和 `saveobjt` 删除分支; -6. 加入事务回滚、环路检测、日志和错误聚合。 - -### 第三阶段:前端 - -1. 删除条件 AST、引用规则和旧作用域逻辑; -2. 将规则编辑器改为 SQL 编辑器; -3. 删除规则面板与模块关联面板收敛为“删除策略”入口; -4. 删除 `ModuleRelationPanel.vue` 的多对一 `b_on_delete` 禁用、重置、警告和 `_hasRefRule` 联动; -5. 删除 `FmsModuleListPage.vue` 的 `b_view_table` 删除回退,空 `b_save_table` 时拒绝删除; -6. 列表删除改为调用统一删除接口; -7. 更新相关测试。 - -### 第四阶段:验收 - -- 单条删除成功; -- 批量删除成功; -- SQL 命中时整批拒绝; -- `restrict` 外部引用生效; -- `cascade` 子记录全部删除; -- 多层级联按子到父顺序删除; -- 关系环路被拦截; -- `saveobjt` 中模块目标与列表删除使用相同规则,直接目标不触发模块检查; -- 文件和其他无模块表可以走直接删除路径; -- 任一失败都能完整回滚。 - -## 10. 后续扩展边界 - -后续增加归档时,只增加新的关联处置值和对应执行器;不改变 SQL 规则的“返回行即拦截”语义。 - -后续增加“审核后不能修改”“保存前校验”“自动冲销”等能力时,重新评估是否需要新的策略表或服务,不在本次设计中预留 `b_hook` 或通用规则字段。 diff --git a/code/fms/FMS工作流与审批设计.md b/code/fms/FMS工作流与审批设计.md index f14e7c1c..fbee613f 100644 --- a/code/fms/FMS工作流与审批设计.md +++ b/code/fms/FMS工作流与审批设计.md @@ -23,11 +23,11 @@ 本文遵循以下已有设计: -- 表名使用小写 `snake_case`,系统表使用 `s_` 前缀; +- 表名使用小写 `snake_case`;工作流是独立子系统,不属于系统内核,表统一使用 `wf_` 前缀,不使用 `s_`; - 关系型表保存稳定、需要查询和关联的对象;变化频繁的配置使用 `nvarchar(max)` JSON; - 不依赖数据库外键、触发器和 `CHECK` 约束表达业务规则; - 权限由 `s_power`、`s_user_power`、`s_user_field_power`、`s_user_data_power` 和业务层权限引擎共同完成; -- 通用审计使用 `s_log_audit` 和 `s_log_audit_field`,工作流动作历史单独使用 `s_wf_action`; +- 通用审计使用 `s_log_audit` 和 `s_log_audit_field`,工作流动作历史单独使用 `wf_action`; - 业务表主键可以是 bigint 雪花 ID,工作流运行对象使用字符串保存业务主键,避免依赖业务表物理主键类型。 相关文档: @@ -44,7 +44,7 @@ 1. 一个模块可以绑定多个流程,按照业务事件、组织和业务条件选择流程; 2. 流程定义可编辑、校验、模拟、发布和停用; 3. 已发布版本不可修改,运行中的流程固定使用提交时的版本; -4. 支持串行审批、并行审批、会签、任一人通过、全部通过和 N 人通过; +4. 支持串行审批、并行审批、会签、串行会签、任一人通过、全部通过、N 人通过和按比例通过; 5. 支持指定用户、发起人直属领导、部门负责人、岗位、业务角色和业务字段用户; 6. 支持条件分支,条件由结构化规则表达,不允许前端直接拼接 SQL; 7. 审批权限同时受动作权限、当前待办任务和数据范围限制; @@ -58,6 +58,7 @@ - 流程路由; - 待办任务; +- 抄送记录; - 审批人解析; - 审批动作; - 流程状态和业务状态投影; @@ -141,6 +142,7 @@ retired 已停用,不再接受新实例 ```text start 开始 user_task 人工审批 +cc_task 抄送 exclusive_gateway 条件分支 parallel_gateway 并行分支 service_task 后端服务动作 @@ -176,10 +178,17 @@ claim 抢占待办 transfer 转交 delegate 委托处理 reassign 管理员转派 +add_approver 加签 +urge 催办 +cc 抄送(系统动作) ``` 数据库保存稳定编码,页面文案通过多语言资源显示。 +### 4.9 抄送(Cc) + +抄送把流程的进展或结果告知不参与审批的人。抄送不产生待办,不参与节点判定,只产生可查询的抄送记录和 outbox 通知。抄送记录使用独立的 `wf_cc`,不写入 `wf_task`。 + ## 5. 总体架构 ```text @@ -211,14 +220,14 @@ POST /workflow/instances/{id}/actions { "action": "approve", "comment": "同意", "idempotencyKey": "..." } ``` -前端不能直接修改 `s_wf_instance`、`s_wf_task` 或业务表状态。 +前端不能直接修改 `wf_instance`、`wf_task`、`wf_cc` 或业务表状态。 ## 6. 配置模型 ### 6.1 流程定义 ```sql -create table dbo.s_wf_definition ( +create table dbo.wf_definition ( b_id varchar(50) not null primary key, -- 稳定流程编码 b_name nvarchar(200) not null, -- 流程名称 b_i18n varchar(150) null, -- 多语言资源键 @@ -230,20 +239,20 @@ create table dbo.s_wf_definition ( b_updated_at datetime2 null ); -create index ix_s_wf_definition_canuse - on dbo.s_wf_definition (b_canuse, b_id); +create index ix_wf_definition_canuse + on dbo.wf_definition (b_canuse, b_id); ``` -`b_id` 创建后原则上不可修改。流程定义是逻辑身份,具体执行内容放在 `s_wf_version`。 +`b_id` 创建后原则上不可修改。流程定义是逻辑身份,具体执行内容放在 `wf_version`。 ### 6.2 流程版本 ```sql -create table dbo.s_wf_version ( +create table dbo.wf_version ( b_id varchar(50) not null primary key, -- 版本 ID b_definition_id varchar(50) not null, -- 流程定义编码 b_version_no int not null, -- 版本序号 - b_status varchar(20) not null default 'draft', -- draft/published/retired + b_status varchar(20) not null default 'draft', -- 版本状态:draft(草稿)/published(已发布)/retired(已停用) b_schema_version int not null default 1, -- 配置结构版本 b_canvas_json nvarchar(max) null, -- 设计器布局,不参与执行 b_checksum varchar(128) null, -- 发布时计算的配置摘要 @@ -256,11 +265,11 @@ create table dbo.s_wf_version ( b_updated_at datetime2 null ); -create unique index ux_s_wf_version_no - on dbo.s_wf_version (b_definition_id, b_version_no); +create unique index ux_wf_version_no + on dbo.wf_version (b_definition_id, b_version_no); -create index ix_s_wf_version_status - on dbo.s_wf_version (b_definition_id, b_status, b_version_no); +create index ix_wf_version_status + on dbo.wf_version (b_definition_id, b_status, b_version_no); ``` 发布时必须完成: @@ -275,10 +284,10 @@ create index ix_s_wf_version_status ### 6.3 流程绑定 ```sql -create table dbo.s_wf_binding ( +create table dbo.wf_binding ( b_id varchar(50) not null primary key, -- 绑定 ID b_module_id varchar(50) not null, -- data / virtual 模块 - b_event varchar(30) not null, -- submit / resubmit / change + b_event varchar(30) not null, -- 业务事件:submit(送审)/resubmit(重新提交)/change(变更) b_version_id varchar(50) not null, -- 已发布流程版本 b_match_json nvarchar(max) null, -- 业务匹配条件 AST b_org_scope_json nvarchar(max) null, -- 公司、组织范围 @@ -295,8 +304,8 @@ create table dbo.s_wf_binding ( b_updated_at datetime2 null ); -create index ix_s_wf_binding_select - on dbo.s_wf_binding ( +create index ix_wf_binding_select + on dbo.wf_binding ( b_module_id, b_event, b_canuse, @@ -311,26 +320,26 @@ create index ix_s_wf_binding_select ### 6.4 流程节点 ```sql -create table dbo.s_wf_node ( +create table dbo.wf_node ( b_id varchar(50) not null primary key, -- 节点 ID b_version_id varchar(50) not null, b_node_key varchar(80) not null, -- 版本内稳定编码 b_name nvarchar(200) not null, b_i18n varchar(150) null, - b_node_type varchar(30) not null, -- start/user_task/... - b_approval_mode varchar(30) null, -- single/any/all/n_of_m/percentage - b_reject_mode varchar(30) null, -- terminate/return_initiator/return_previous + b_node_type varchar(30) not null, -- 节点类型:start(开始)/user_task(人工审批)/cc_task(抄送)/exclusive_gateway(条件分支)/parallel_gateway(并行分支)/service_task(服务节点)/end(结束) + b_approval_mode varchar(30) null, -- 审批方式:single(单人)/any(或签)/all(会签)/serial(串行会签)/n_of_m(N 人通过)/percentage(按比例通过) + b_reject_mode varchar(30) null, -- 驳回策略:terminate(驳回结束)/return_initiator(退回发起人)/return_previous(退回上一节点) b_due_hours decimal(10,2) null, b_config_json nvarchar(max) null, -- 节点扩展配置 b_xh int not null default 0, b_canuse tinyint not null default 1 ); -create unique index ux_s_wf_node_key - on dbo.s_wf_node (b_version_id, b_node_key); +create unique index ux_wf_node_key + on dbo.wf_node (b_version_id, b_node_key); -create index ix_s_wf_node_type - on dbo.s_wf_node (b_version_id, b_node_type, b_xh, b_node_key); +create index ix_wf_node_type + on dbo.wf_node (b_version_id, b_node_type, b_xh, b_node_key); ``` 同一版本内 `b_node_key` 必须稳定。设计器可以显示名称,但节点关联和历史记录使用 `b_node_key`。 @@ -345,6 +354,7 @@ create index ix_s_wf_node_type "allowClaim": false, "commentRequired": true, "requiredCount": 2, + "requiredRatio": 0.6, "formPolicy": { "readonly": ["b_amount"], "required": ["b_reason"] @@ -352,12 +362,12 @@ create index ix_s_wf_node_type } ``` -`requiredCount` 只对 `n_of_m` 生效,不能用界面约定替代后端校验。 +`requiredCount` 只对 `n_of_m` 生效,`requiredRatio` 只对 `percentage` 生效,都不能用界面约定替代后端校验。 ### 6.5 节点连线 ```sql -create table dbo.s_wf_edge ( +create table dbo.wf_edge ( b_id varchar(50) not null primary key, b_version_id varchar(50) not null, b_from_node_key varchar(80) not null, @@ -368,11 +378,11 @@ create table dbo.s_wf_edge ( b_bz nvarchar(1000) null ); -create index ix_s_wf_edge_from - on dbo.s_wf_edge (b_version_id, b_from_node_key, b_canuse, b_priority, b_to_node_key); +create index ix_wf_edge_from + on dbo.wf_edge (b_version_id, b_from_node_key, b_canuse, b_priority, b_to_node_key); -create index ix_s_wf_edge_to - on dbo.s_wf_edge (b_version_id, b_to_node_key, b_canuse, b_priority, b_from_node_key); +create index ix_wf_edge_to + on dbo.wf_edge (b_version_id, b_to_node_key, b_canuse, b_priority, b_from_node_key); ``` 同一个来源节点的条件连线按 `b_priority` 从小到大判断。发布校验要求条件分支具有明确的默认出口,避免所有条件不满足时流程卡死。 @@ -380,23 +390,23 @@ create index ix_s_wf_edge_to ### 6.6 审批人规则 ```sql -create table dbo.s_wf_actor_rule ( +create table dbo.wf_actor_rule ( b_id varchar(50) not null primary key, b_version_id varchar(50) not null, b_node_key varchar(80) not null, - b_actor_type varchar(30) not null, -- user/initiator/manager/position/role/field_user/custom + b_actor_type varchar(30) not null, -- 解析类型:user(指定用户)/initiator(发起人)/initiator_manager(发起人直属领导)/dept_manager(部门负责人)/position(岗位)/org_role(组织角色)/field_user(业务字段用户)/custom(自定义解析器) b_actor_ref varchar(150) null, -- 用户、岗位、角色或字段编码 b_resolve_json nvarchar(max) null, -- 解析参数 b_condition_json nvarchar(max) null, -- 规则适用条件 AST - b_fallback_type varchar(30) null, -- none/admin/initiator/custom + b_fallback_type varchar(30) null, -- 解析为空时的兜底:none(不兜底)/admin(管理员)/initiator(发起人)/custom(自定义) b_fallback_ref varchar(150) null, b_priority int not null default 0, b_canuse tinyint not null default 1, b_bz nvarchar(1000) null ); -create index ix_s_wf_actor_rule_node - on dbo.s_wf_actor_rule (b_version_id, b_node_key, b_canuse, b_priority, b_actor_type); +create index ix_wf_actor_rule_node + on dbo.wf_actor_rule (b_version_id, b_node_key, b_canuse, b_priority, b_actor_type); ``` 建议支持以下 `b_actor_type`: @@ -463,19 +473,19 @@ between ### 8.1 流程实例 ```sql -create table dbo.s_wf_instance ( +create table dbo.wf_instance ( b_id uniqueidentifier not null primary key, -- 应用层生成 UUIDv7 b_definition_id varchar(50) not null, b_version_id varchar(50) not null, b_binding_id varchar(50) not null, b_module_id varchar(50) not null, - b_event varchar(30) not null, + b_event varchar(30) not null, -- 业务事件:submit(送审)/resubmit(重新提交)/change(变更) b_business_id varchar(50) not null, -- 业务主键统一按字符串保存 b_business_no nvarchar(200) null, b_org_id varchar(50) null, b_initiator_id varchar(50) not null, b_initiator_dept_id varchar(50) null, - b_status varchar(30) not null, -- running/approved/rejected/returned/... + b_status varchar(30) not null, -- 实例状态:running(运行中)/approved(已通过)/rejected(已驳回)/returned(已退回)/withdrawn(已撤回)/cancelled(已取消)/expired(已过期)/error(异常待处理) b_context_json nvarchar(max) null, -- 运行变量 b_snapshot_json nvarchar(max) null, -- 提交时业务快照 b_current_summary_json nvarchar(max) null, -- 列表展示用的当前节点摘要 @@ -489,14 +499,14 @@ create table dbo.s_wf_instance ( b_updated_at datetime2 null ); -create index ix_s_wf_instance_business - on dbo.s_wf_instance (b_module_id, b_business_id, b_event, b_status, b_id); +create index ix_wf_instance_business + on dbo.wf_instance (b_module_id, b_business_id, b_event, b_status, b_id); -create index ix_s_wf_instance_initiator - on dbo.s_wf_instance (b_initiator_id, b_status, b_started_at, b_id); +create index ix_wf_instance_initiator + on dbo.wf_instance (b_initiator_id, b_status, b_started_at, b_id); -create index ix_s_wf_instance_version - on dbo.s_wf_instance (b_version_id, b_started_at, b_id); +create index ix_wf_instance_version + on dbo.wf_instance (b_version_id, b_started_at, b_id); ``` 流程实例状态建议使用稳定编码: @@ -517,13 +527,13 @@ error 异常待处理 ### 8.2 节点运行 ```sql -create table dbo.s_wf_node_run ( +create table dbo.wf_node_run ( b_id uniqueidentifier not null primary key, b_instance_id uniqueidentifier not null, b_node_key varchar(80) not null, b_run_no int not null default 1, - b_status varchar(30) not null, -- pending/running/completed/skipped - b_required_count int null, + b_status varchar(30) not null, -- 节点状态:pending(待激活)/running(运行中)/completed(已完成)/skipped(已跳过) + b_required_count int null, -- 应签人数,按比例时由后端在节点激活时换算 b_completed_count int not null default 0, b_rejected_count int not null default 0, b_entered_at datetime2 null, @@ -532,26 +542,27 @@ create table dbo.s_wf_node_run ( b_created_at datetime2 null ); -create index ix_s_wf_node_run_instance - on dbo.s_wf_node_run (b_instance_id, b_status, b_node_key, b_run_no, b_id); +create index ix_wf_node_run_instance + on dbo.wf_node_run (b_instance_id, b_status, b_node_key, b_run_no, b_id); ``` `b_run_no` 用于支持退回后重新进入同一节点、循环节点和重新提交。 ### 8.3 待办任务 -一名实际审批人一行任务。会签和或签通过同一个 `b_node_run_id` 进行聚合判断。 +一名实际审批人一行任务。会签、或签和串行会签通过同一个 `b_node_run_id` 进行聚合判断;串行会签的未激活任务使用 `waiting` 状态,按 `b_seq` 顺序逐个激活。 ```sql -create table dbo.s_wf_task ( +create table dbo.wf_task ( b_id uniqueidentifier not null primary key, b_instance_id uniqueidentifier not null, b_node_run_id uniqueidentifier not null, b_node_key varchar(80) not null, b_actor_rule_id varchar(50) null, - b_assignee_type varchar(30) not null, -- user/position/role/代理等 + b_assignee_type varchar(30) not null, -- 受理人类型:user(指定用户)/position(岗位)/role(角色)/delegate(委托) b_assignee_id varchar(50) not null, -- 实际用户编码 - b_status varchar(30) not null, -- pending/claimed/approved/... + b_seq int not null default 0, -- 同节点内处理顺序,串行会签生效 + b_status varchar(30) not null, -- 任务状态:waiting(未激活)/pending(待处理)/claimed(已抢占)/approved(已通过)/rejected(已驳回)/cancelled(已取消)/expired(已过期) b_due_at datetime2 null, b_claimed_by varchar(50) null, b_claimed_at datetime2 null, @@ -562,14 +573,14 @@ create table dbo.s_wf_task ( b_updated_at datetime2 null ); -create index ix_s_wf_task_assignee - on dbo.s_wf_task (b_assignee_id, b_status, b_due_at, b_id); +create index ix_wf_task_assignee + on dbo.wf_task (b_assignee_id, b_status, b_due_at, b_id); -create index ix_s_wf_task_instance - on dbo.s_wf_task (b_instance_id, b_status, b_node_run_id, b_id); +create index ix_wf_task_instance + on dbo.wf_task (b_instance_id, b_status, b_node_run_id, b_id); -create index ix_s_wf_task_due - on dbo.s_wf_task (b_status, b_due_at, b_id); +create index ix_wf_task_due + on dbo.wf_task (b_status, b_due_at, b_id); ``` 如果组织角色解析出大量候选人,仍然要在节点激活时形成可追溯的候选任务,不能只在页面实时计算“当前谁可以审批”。 @@ -577,12 +588,12 @@ create index ix_s_wf_task_due ### 8.4 审批动作 ```sql -create table dbo.s_wf_action ( +create table dbo.wf_action ( b_id uniqueidentifier not null primary key, b_instance_id uniqueidentifier not null, b_node_run_id uniqueidentifier null, b_task_id uniqueidentifier null, - b_action varchar(30) not null, + b_action varchar(30) not null, -- 动作编码:submit(送审)/approve(通过)/reject(驳回)/return(退回)/withdraw(撤回)/cancel(取消)/claim(抢占)/transfer(转交)/delegate(委托)/reassign(转派)/add_approver(加签)/urge(催办)/cc(抄送) b_operator_id varchar(50) null, b_from_status varchar(30) null, b_to_status varchar(30) null, @@ -593,19 +604,19 @@ create table dbo.s_wf_action ( b_occurdatetime datetime2 not null ); -create index ix_s_wf_action_instance - on dbo.s_wf_action (b_instance_id, b_occurdatetime, b_id); +create index ix_wf_action_instance + on dbo.wf_action (b_instance_id, b_occurdatetime, b_id); -create index ix_s_wf_action_operator - on dbo.s_wf_action (b_operator_id, b_occurdatetime, b_id); +create index ix_wf_action_operator + on dbo.wf_action (b_operator_id, b_occurdatetime, b_id); ``` -`s_wf_action` 只追加,不更新历史动作。撤回、转交和管理员转派也必须形成动作记录。 +`wf_action` 只追加,不更新历史动作。撤回、转交和管理员转派也必须形成动作记录。 -### 8.5 委托和通知消息 +### 8.5 委托、抄送和通知消息 ```sql -create table dbo.s_wf_delegation ( +create table dbo.wf_delegation ( b_id varchar(50) not null primary key, b_from_user_id varchar(50) not null, b_to_user_id varchar(50) not null, @@ -620,24 +631,46 @@ create table dbo.s_wf_delegation ( b_updated_at datetime2 null ); -create table dbo.s_wf_outbox ( +create index ix_wf_delegation_user + on dbo.wf_delegation (b_from_user_id, b_canuse, b_start_at, b_end_at, b_to_user_id); + +create table dbo.wf_cc ( + b_id uniqueidentifier not null primary key, + b_instance_id uniqueidentifier not null, + b_node_run_id uniqueidentifier null, + b_node_key varchar(80) null, -- 来源节点 + b_recipient_id varchar(50) not null, -- 抄送接收人 + b_actor_rule_id varchar(50) null, -- 命中的抄送规则 + b_status varchar(20) not null default 'unread', -- 阅读状态:unread(未读)/read(已读) + b_read_at datetime2 null, + b_rule_snapshot_json nvarchar(max) null, + b_created_at datetime2 null +); + +create index ix_wf_cc_recipient + on dbo.wf_cc (b_recipient_id, b_status, b_created_at, b_id); + +create index ix_wf_cc_instance + on dbo.wf_cc (b_instance_id, b_created_at, b_id); + +create table dbo.wf_outbox ( b_id uniqueidentifier not null primary key, b_event_code varchar(80) not null, - b_aggregate_type varchar(30) not null, -- instance/task + b_aggregate_type varchar(30) not null, -- 聚合类型:instance(流程实例)/task(待办任务) b_aggregate_id varchar(50) not null, b_payload_json nvarchar(max) not null, - b_status varchar(20) not null default 'pending', -- pending/sent/failed + b_status varchar(20) not null default 'pending', -- 发送状态:pending(待发送)/sent(已发送)/failed(发送失败) b_retry_count int not null default 0, b_next_retry_at datetime2 null, b_last_error nvarchar(2000) null, b_occurdatetime datetime2 not null ); -create index ix_s_wf_outbox_dispatch - on dbo.s_wf_outbox (b_status, b_next_retry_at, b_occurdatetime, b_id); +create index ix_wf_outbox_dispatch + on dbo.wf_outbox (b_status, b_next_retry_at, b_occurdatetime, b_id); ``` -通知、站内信、邮件和企业微信等外部动作通过 outbox 异步发送,不阻塞审批事务。关键的实例、任务和业务状态仍然在同一数据库事务中提交。 +通知、站内信、邮件和企业微信等外部动作通过 outbox 异步发送,不阻塞审批事务。关键的实例、任务、抄送记录和业务状态仍然在同一数据库事务中提交。 ## 9. 审批方式与路由语义 @@ -647,7 +680,7 @@ create index ix_s_wf_outbox_dispatch ### 9.2 任一人通过 -为所有候选人创建任务。任意一个人通过后,节点完成,其余待办自动变为 `cancelled`,并写入系统动作。 +为所有候选人创建任务。任意一个人通过后,节点完成,其余待办自动变为 `cancelled`,并写入系统动作。或签的驳回同样即时生效:第一位处理人驳回时立即按 `b_reject_mode` 处理(见 9.7),其余待办自动取消。 ### 9.3 全部通过 @@ -655,11 +688,11 @@ create index ix_s_wf_outbox_dispatch ### 9.4 N 人通过或按比例通过 -节点运行记录保存 `required_count`、`completed_count` 和 `rejected_count`。计数必须由后端在锁定节点运行记录后完成,不能由前端计算。 +节点运行记录保存 `required_count`、`completed_count` 和 `rejected_count`。计数必须由后端在锁定节点运行记录后完成,不能由前端计算。按比例通过时,节点激活必须把 `requiredRatio` 换算为 `required_count` 并写入快照,避免审批人数变化后票数漂移。 ### 9.5 条件分支 -条件分支通过 `s_wf_edge.b_condition_json` 表达。路由时记录: +条件分支通过 `wf_edge.b_condition_json` 表达。路由时记录: ```text 命中的边 ID @@ -670,6 +703,50 @@ create index ix_s_wf_outbox_dispatch 这样在业务条件后来变化时,历史流程仍然可以解释为什么走了某条路径。 +### 9.6 串行会签 + +`serial` 模式按 `wf_task.b_seq` 顺序逐个激活审批人: + +- 审批顺序由 `wf_actor_rule.b_priority` 和规则解析顺序决定,生成任务时写入 `b_seq`; +- 同一时刻只有一名审批人的任务为 `pending`,其余为 `waiting`; +- 当前审批人通过后激活下一名,全部通过后节点完成; +- 任一环节驳回按 `b_reject_mode` 处理(见 9.7),未激活任务置为 `cancelled`; +- 只有 `pending` 任务出现在待办列表,`waiting` 任务靠任务快照解释“即将轮到我”。 + +未激活任务的顺序允许管理员调整,但必须写入动作记录。 + +### 9.7 驳回和否决语义 + +驳回是即时生效的:任何审批方式下,节点内出现第一个驳回时立即按 `b_reject_mode` 处理,其余未完成任务自动置为 `cancelled` 并写入系统动作。 + +```text +terminate 实例进入 rejected,本次流程结束,业务状态投影为已驳回(即“一票否决”) +return_initiator 实例进入 returned,退回发起人修改后重新提交 +return_previous 回到上一节点重新审批 +``` + +- 会签、N 人通过和按比例通过节点默认使用 `terminate`,即任一驳回即一票否决; +- 驳回必须记录操作人、命中任务和当时的节点计数; +- “驳回不影响其他人”不是驳回语义;需要停止本次审批时应由发起人撤回。 + +### 9.8 加签 + +加签在当前节点运行中追加审批人: + +- 加签人必须拥有 `action.{module}.add_approver`,且为当前节点待办人或流程管理员; +- 加签任务挂在同一个 `b_node_run_id` 下,按节点 `b_approval_mode` 参与判定; +- `n_of_m` 和 `percentage` 的 `required_count`、`requiredRatio` 不因加签自动改变,需要调整时必须在动作载荷中显式声明; +- 串行会签的加签任务按 `b_seq` 插入到指定位置,其余任务顺序顺延; +- 加签必须写入 `wf_action`,载荷保存加签理由、加签人和插入位置。 + +### 9.9 抄送 + +抄送节点激活后立即解析抄送人、写入 `wf_cc`、投递 outbox 通知并自动完成,不产生待办,也不参与节点计数。 + +- 抄送规则复用 `wf_actor_rule`,支持与审批人相同的解析类型; +- 抄送人不写入 `wf_task`,不进入审批人的待办列表; +- 抄送记录独立于审批动作历史,支持“抄送我的”查询和已读标记。 + ## 10. 权限和安全模型 ### 10.1 三层校验 @@ -695,6 +772,8 @@ action.{module}.withdraw action.{module}.transfer action.{module}.delegate action.{module}.reassign +action.{module}.add_approver +action.{module}.urge action.{module}.workflow_admin ``` @@ -746,12 +825,12 @@ excludeCreator = true 锁定业务记录 → 校验当前状态和版本 → 校验用户权限与任务 -→ 写 s_wf_action -→ 更新 s_wf_task / s_wf_node_run +→ 写 wf_action +→ 更新 wf_task / wf_node_run → 创建下一节点任务或结束实例 → 更新业务表状态投影 → 写 s_log_audit / s_log_audit_field -→ 写 s_wf_outbox +→ 写 wf_outbox → 提交 ``` @@ -771,7 +850,7 @@ request_id + operator_id + action ### 12.2 乐观锁 -`s_wf_instance.b_row_version` 用于防止多个管理员或审批人同时推进同一个实例。更新时必须带上读取时的版本号。 +`wf_instance.b_row_version` 用于防止多个管理员或审批人同时推进同一个实例。更新时必须带上读取时的版本号。 ### 12.3 任务抢占 @@ -791,7 +870,7 @@ request_id + operator_id + action error / waiting_admin_resolve ``` -管理员可以补充审批人或转派,但必须形成管理员动作和审计记录。 +管理员可以通过加签补充审批人或转派,但必须形成管理员动作和审计记录。 ### 12.5 外部通知失败 @@ -812,6 +891,9 @@ POST /api/workflows/tasks/{taskId}/actions POST /api/workflows/instances/{instanceId}/withdraw POST /api/workflows/tasks/{taskId}/transfer POST /api/workflows/tasks/{taskId}/delegate +POST /api/workflows/tasks/{taskId}/add-approver +POST /api/workflows/instances/{instanceId}/urge +GET /api/workflows/cc ``` 审批动作请求示例: @@ -882,16 +964,23 @@ POST /api/workflows/tasks/{taskId}/delegate ### 14.4 流程待办和历史 -业务用户使用独立的: +业务用户使用独立的入口: ```text -我的待办 -我发起的 -我已处理 -流程监控 -审批历史 +我的待办 审批中心菜单,读 wf_task(当前用户的 pending/claimed 任务) +我发起的 审批中心菜单,读 wf_instance(b_initiator_id = 当前用户) +我已处理 审批中心菜单,读 wf_task(当前用户已完成的任务) +抄送我的 审批中心菜单,读 wf_cc +流程监控 不开独立菜单,作为「流程管理」页内的页签,供管理员查看 +审批历史 不开独立菜单,作为业务单据详情页的时间线 ``` +菜单归属: + +- 「我的待办 / 我发起的 / 我已处理 / 抄送我的」是**审批中心**(一级菜单)下的四个页面,面向全员,用 `menu.approval*` 授权; +- 「流程监控」并入「系统管理 → 流程管理」页,与流程配置、委派管理同页,共用 `menu.workflow` 授权。配置和监控同属管理员场景,不再单独开菜单;仅当监控受众与配置受众分离(例如业务主管要看本部门积压)时,才拆到审批中心下并单独授权; +- 「审批历史」是单据详情页里的时间线(读 `wf_action`),脱离单据没有使用场景,不做独立入口。 + 业务详情页只显示当前流程状态、当前待办和时间线,不直接读取流程配置表判断按钮是否可用。 ## 15. 旧数据迁移 @@ -900,9 +989,9 @@ POST /api/workflows/tasks/{taskId}/delegate | 旧字段 | 新模型 | | --- | --- | -| `sh_set.b_module_id` | `s_wf_binding.b_module_id` | +| `sh_set.b_module_id` | `wf_binding.b_module_id` | | `sh_set.b_level` | 线性节点的顺序和节点编码 | -| `sh_set.b_user_id` | `s_wf_actor_rule` 的 `user` 规则 | +| `sh_set.b_user_id` | `wf_actor_rule` 的 `user` 规则 | | `sh_set.b_condition` | 条件 AST;不能自动转换的进入人工复核 | | `sh_set.b_logic`、`b_leap` | 节点类型、连线条件或退回策略,不能机械照搬 | | `sh_set.b_bz` | 节点或审批人规则备注 | @@ -911,13 +1000,13 @@ POST /api/workflows/tasks/{taskId}/delegate | 旧字段 | 新模型 | | --- | --- | -| `sh_record.b_dj_id` | `s_wf_instance.b_business_id` | -| `sh_record.b_level` | `s_wf_node_run.b_node_key` / `b_run_no` | -| `b_tosh_useid` | 多条 `s_wf_task` | +| `sh_record.b_dj_id` | `wf_instance.b_business_id` | +| `sh_record.b_level` | `wf_node_run.b_node_key` / `b_run_no` | +| `b_tosh_useid` | 多条 `wf_task` | | `b_link_useid` | 已处理用户快照或动作载荷 | -| `b_dosh_useid` | `s_wf_action.b_operator_id` 历史动作 | -| `b_sh_date` | `s_wf_action.b_occurdatetime` | -| `b_sh_memo` | `s_wf_action.b_comment` | +| `b_dosh_useid` | `wf_action.b_operator_id` 历史动作 | +| `b_sh_date` | `wf_action.b_occurdatetime` | +| `b_sh_memo` | `wf_action.b_comment` | | 业务表 `b_state` | 保留为业务状态投影 | 历史迁移不应篡改原始业务状态和原始日志。无法完整映射的旧数据可以以“历史兼容实例”导入,并在 `b_payload_json` 中保存原始字段。 @@ -936,17 +1025,19 @@ POST /api/workflows/tasks/{taskId}/delegate ## 16. 分阶段落地建议 +涉及枚举的能力——审批方式(`b_approval_mode`)、节点类型(`b_node_type`)和动作编码——在第一阶段一次做完。枚举值在运行后再扩充,会导致历史数据、界面文案和后端分支各自维护,代价高于一次实现。 + ### 第一阶段:企业级基础内核 建议先实现: - 流程定义、版本、发布和停用; - 模块和业务事件绑定; -- 开始、人工审批、条件分支、结束节点; -- 串行、任一人、全部人; +- 开始、人工审批、抄送、条件分支、结束节点; +- 单人、或签、会签、串行会签、N 人通过、按比例通过; - 指定用户、发起人领导、部门负责人; -- 提交、通过、驳回、退回、撤回; -- 流程实例、节点运行、待办和动作历史; +- 提交、通过、驳回、退回、撤回、加签、催办; +- 流程实例、节点运行、待办、抄送和动作历史; - 动作权限、任务权限和数据范围三层校验; - 业务状态投影、审计日志和 outbox 通知。 @@ -954,7 +1045,6 @@ POST /api/workflows/tasks/{taskId}/delegate - 岗位和组织角色审批人; - 委托、转交、管理员转派; -- N 人会签、百分比会签; - 审批期限、提醒和升级; - 流程模拟和审批人解析预览; - 流程监控和 SLA 报表。 @@ -976,6 +1066,8 @@ POST /api/workflows/tasks/{taskId}/delegate - 是否至少存在一个结束节点; - 所有节点是否可达; - 所有人工节点是否存在有效审批人规则; +- 所有抄送节点是否存在有效抄送人规则; +- 串行会签节点的审批人顺序来源是否明确; - 所有条件字段是否存在于 `s_field`; - 是否存在没有默认出口的条件分支; - 是否存在重复生效的流程绑定; @@ -1021,7 +1113,7 @@ LogicFlow FMS Workflow Service └── 负责流程校验、发布、审批人解析、实例推进和事务 -s_wf_definition / s_wf_version / s_wf_node / s_wf_edge +wf_definition / wf_version / wf_node / wf_edge └── 负责保存流程领域模型 ``` @@ -1106,7 +1198,7 @@ logicFlowToWorkflowDraft(graph) validateWorkflowDomain(version) ``` -`s_wf_node` 和 `s_wf_edge` 是可执行流程的来源,`b_canvas_json` 只保存节点坐标、画布缩放、分组位置等界面布局信息。即使以后更换为 X6,也不能改变流程节点编码、审批规则和运行实例语义。 +`wf_node` 和 `wf_edge` 是可执行流程的来源,`b_canvas_json` 只保存节点坐标、画布缩放、分组位置等界面布局信息。即使以后更换为 X6,也不能改变流程节点编码、审批规则和运行实例语义。 ### 19.5 LogicFlow 节点约定 @@ -1115,6 +1207,7 @@ validateWorkflowDomain(version) ```text fms-start fms-user-task +fms-cc-task fms-exclusive-gateway fms-parallel-gateway fms-service-task @@ -1126,13 +1219,14 @@ LogicFlow 的节点类型只是前端渲染类型,后端落库仍使用: ```text start user_task +cc_task exclusive_gateway parallel_gateway service_task end ``` -人工审批节点的审批人、审批方式、时限和表单策略在右侧属性面板编辑,保存到 `s_wf_node` 和 `s_wf_actor_rule`,不保存为节点显示文本。 +人工审批节点的审批人、审批方式、时限和表单策略在右侧属性面板编辑,保存到 `wf_node` 和 `wf_actor_rule`,不保存为节点显示文本。 ### 19.6 编辑器校验边界 @@ -1166,13 +1260,38 @@ LogicFlowCanvasAdapter → X6CanvasAdapter 以下内容不应改变: -- `s_wf_definition`; -- `s_wf_version`; -- `s_wf_node`; -- `s_wf_edge`; -- `s_wf_actor_rule`; -- `s_wf_instance`、`s_wf_task`、`s_wf_action`; +- `wf_definition`; +- `wf_version`; +- `wf_node`; +- `wf_edge`; +- `wf_actor_rule`; +- `wf_instance`、`wf_task`、`wf_action`; - 流程发布和运行接口; - 审批历史和审计记录。 因此,LogicFlow 是当前阶段的流程设计器选型,而不是 FMS 工作流领域模型的依赖。 + +## 20. 签核能力清单 + +本节把常见签核能力映射到本设计的配置、动作和表,作为能力边界声明。 + +| 能力 | 支持情况 | 实现方式 | +| --- | --- | --- | +| 会签(全部通过) | 支持 | `b_approval_mode = 'all'`,同节点多任务聚合,见 9.3 | +| 或签(任一人通过) | 支持 | `'any'`,第一票决定,其余任务自动取消,见 9.2 | +| 穿行会签(串行会签) | 支持 | `'serial'`,按 `wf_task.b_seq` 逐个激活,见 9.6 | +| N 人通过 | 支持 | `'n_of_m'`,`requiredCount` 由后端锁定计算,见 9.4 | +| 比例签 | 支持 | `'percentage'`,`requiredRatio` 在节点激活时换算为 `required_count`,见 9.4 | +| 一票否决 | 支持 | 驳回即时生效,`b_reject_mode = 'terminate'`,见 9.7 | +| 转办/转交 | 支持 | 动作 `transfer`,任务级转交并强制留痕,见 8.4 | +| 委托 | 支持 | `wf_delegation` + 动作 `delegate`,支持时间范围和范围过滤,见 8.5 | +| 加签 | 支持 | 动作 `add_approver`,向当前节点运行追加任务,见 9.8 | +| 催办 | 支持 | 动作 `urge`,通过 outbox 发送提醒,不改变任务状态 | +| 抄送 | 支持 | `cc_task` 节点 + `wf_cc` 记录 + outbox 通知,见 9.9 | +| 跳转(指定任意节点) | 不支持 | 见下方说明 | + +跳转不是遗漏,而是有意排除:流程推进只允许沿已发布版本的连线进行。允许运行中跳到任意节点,会绕过节点校验、审批人解析和动作权限,并让历史流程无法解释为什么走了这条路径。需要类似效果时使用: + +- 发起人撤回后重新提交,产生新实例和新快照; +- 管理员使用任务级 `reassign` 转派,并写入管理员动作; +- 业务上确需提前结束时使用 `cancel`,由系统记录取消原因。 diff --git a/code/fms/FMS新系统核心表结构设计.md b/code/fms/FMS新系统核心表结构设计.md index a54bf445..c6045679 100644 --- a/code/fms/FMS新系统核心表结构设计.md +++ b/code/fms/FMS新系统核心表结构设计.md @@ -1,6 +1,6 @@ # FMS 新系统核心表结构设计 V2 -> 本文用于记录新版核心表结构方案,权限设计见第 14 节。旧版文档保留作为历史参考。 +> 本文用于记录新版核心表结构方案,权限设计见第 14 节,工作流与审批见第 16 节。旧版文档保留作为历史参考。 **本系统以 SQL 为核心驱动。设计时应优先保证查询和数据处理的灵活性,并在各模块预留 SQL 扩展能力,支持传入 SQL、动态拼接查询条件、组合查询及自定义数据处理逻辑,以便通过 SQL 快速适配和解决复杂业务场景。** @@ -1087,3 +1087,385 @@ fms: │ aliyun-oss → 取号 → 凭证直传 → POST /file/save-record 落库 └── 删除:POST /file/delete(删存储 → 删 DB 行) ``` + +## 16. 工作流与审批 + +工作流是独立子系统,不属于系统内核,表统一使用 `wf_` 前缀(第 1.1 节的业务域前缀约定)。配置表保存流程定义、版本、节点、连线、审批人规则和模块绑定;运行表保存实例、节点运行、待办、抄送和不可变动作历史。 + +完整语义、审批方式(或签、会签、串行会签、N 人通过、按比例通过)、驳回策略、加签、催办和抄送规则见《FMS工作流与审批设计》,本文只记录表结构。 + +| 表 | 职责 | +| --- | --- | +| `wf_definition` / `wf_version` | 流程定义与可执行版本 | +| `wf_node` / `wf_edge` / `wf_actor_rule` | 版本内的节点、连线和审批人规则 | +| `wf_binding` | 模块和业务事件到流程版本的绑定 | +| `wf_instance` / `wf_node_run` / `wf_task` | 流程实例、节点运行和待办任务 | +| `wf_action` | 不可变的审批动作历史 | +| `wf_cc` / `wf_delegation` / `wf_outbox` | 抄送记录、委托和通知消息 | + +表结构约定: + +- 配置表使用 `varchar` 业务编码主键;运行表使用应用层生成的 UUIDv7(`uniqueidentifier`),与 `s_log_*` 一致(第 13 节); +- 业务主键统一按字符串保存在 `b_business_id`,不依赖业务表的物理主键类型; +- 状态、类型和动作编码的取值只写在列注释里,不使用 `CHECK` 约束、外键和触发器,合法性由业务层校验(第 1.10 节); +- 已发布版本不可修改,运行中的实例固定引用提交时的 `b_version_id`; +- 变化频繁的配置使用 `nvarchar(max)` JSON,顶层包含 `schemaVersion`(第 1.8 节)。 + +### 16.1 流程定义 + +```sql +-- 流程定义表(稳定逻辑身份,执行内容在版本表) +create table dbo.wf_definition ( + b_id varchar(50) not null primary key, -- 流程定义编码(创建后不可修改) + b_name nvarchar(200) not null, -- 流程名称 + b_i18n varchar(150) null, -- 多语言资源键(s_i18n.b_key) + b_canuse tinyint not null default 1, -- 是否启用(0/1) + b_bz nvarchar(2000) null, -- 备注 + b_created_by varchar(50) null, -- 创建人(用户编码) + b_created_at datetime2 null, -- 创建时间 + b_updated_by varchar(50) null, -- 最后修改人 + b_updated_at datetime2 null -- 最后修改时间 +); + +create index ix_wf_definition_canuse + on dbo.wf_definition (b_canuse, b_id); +``` + +### 16.2 流程版本 + +```sql +-- 流程版本表(draft/published/retired,已发布版本不可原地修改) +create table dbo.wf_version ( + b_id varchar(50) not null primary key, -- 版本 ID + b_definition_id varchar(50) not null, -- 流程定义编码(wf_definition.b_id) + b_version_no int not null, -- 版本序号(同一流程内递增) + b_status varchar(20) not null default 'draft', -- 版本状态:draft(草稿)/published(已发布)/retired(已停用) + b_schema_version int not null default 1, -- 配置结构版本(JSON 结构升级用) + b_canvas_json nvarchar(max) null, -- 设计器布局(节点坐标、缩放),不参与执行 + b_checksum varchar(128) null, -- 发布时计算的配置摘要 + b_published_by varchar(50) null, -- 发布人(用户编码) + b_published_at datetime2 null, -- 发布时间 + b_bz nvarchar(2000) null, -- 备注 + b_created_by varchar(50) null, -- 创建人 + b_created_at datetime2 null, -- 创建时间 + b_updated_by varchar(50) null, -- 最后修改人 + b_updated_at datetime2 null -- 最后修改时间 +); + +create unique index ux_wf_version_no + on dbo.wf_version (b_definition_id, b_version_no); + +create index ix_wf_version_status + on dbo.wf_version (b_definition_id, b_status, b_version_no); +``` + +发布校验由业务层完成:开始/结束节点完整、人工节点有审批人规则、条件字段来自 `s_field`、不存在不可达节点和死循环;发布后禁止修改节点和连线。 + +### 16.3 流程绑定 + +```sql +-- 流程绑定表(模块 + 业务事件 + 条件 → 流程版本) +create table dbo.wf_binding ( + b_id varchar(50) not null primary key, -- 绑定 ID + b_module_id varchar(50) not null, -- 模块编码(s_module.b_id,data/virtual 模块) + b_event varchar(30) not null, -- 业务事件:submit(送审)/resubmit(重新提交)/change(变更) + b_version_id varchar(50) not null, -- 已发布流程版本(wf_version.b_id) + b_match_json nvarchar(max) null, -- 业务匹配条件 AST + b_org_scope_json nvarchar(max) null, -- 公司、组织范围 + b_status_field varchar(50) null, -- 业务状态字段(必须来自 s_field) + b_status_map_json nvarchar(max) null, -- 流程状态到业务状态的映射 + b_priority int not null default 0, -- 匹配优先级(小的先匹配) + b_effective_from datetime2 null, -- 生效开始时间 + b_effective_to datetime2 null, -- 生效结束时间 + b_canuse tinyint not null default 1, -- 是否启用(0/1) + b_bz nvarchar(2000) null, -- 备注 + b_created_by varchar(50) null, -- 创建人 + b_created_at datetime2 null, -- 创建时间 + b_updated_by varchar(50) null, -- 最后修改人 + b_updated_at datetime2 null -- 最后修改时间 +); + +create index ix_wf_binding_select + on dbo.wf_binding (b_module_id, b_event, b_canuse, b_priority, b_effective_from, b_effective_to); +``` + +同一业务上下文只能匹配一个最终流程,优先级冲突由业务层校验。`b_status_field` 用于兼容业务表已有的 `b_state`、`b_status` 字段名。 + +### 16.4 流程节点 + +```sql +-- 流程节点表(版本内节点定义) +create table dbo.wf_node ( + b_id varchar(50) not null primary key, -- 节点 ID + b_version_id varchar(50) not null, -- 流程版本(wf_version.b_id) + b_node_key varchar(80) not null, -- 版本内稳定编码(历史记录引用它) + b_name nvarchar(200) not null, -- 节点名称 + b_i18n varchar(150) null, -- 多语言资源键 + b_node_type varchar(30) not null, -- 节点类型:start(开始)/user_task(人工审批)/cc_task(抄送)/exclusive_gateway(条件分支)/parallel_gateway(并行分支)/service_task(服务节点)/end(结束) + b_approval_mode varchar(30) null, -- 审批方式:single(单人)/any(或签)/all(会签)/serial(串行会签)/n_of_m(N 人通过)/percentage(按比例通过) + b_reject_mode varchar(30) null, -- 驳回策略:terminate(驳回结束)/return_initiator(退回发起人)/return_previous(退回上一节点) + b_due_hours decimal(10,2) null, -- 审批时限(小时) + b_config_json nvarchar(max) null, -- 节点扩展配置(requiredCount、requiredRatio、formPolicy 等) + b_xh int not null default 0, -- 显示顺序 + b_canuse tinyint not null default 1 -- 是否启用(0/1) +); + +create unique index ux_wf_node_key + on dbo.wf_node (b_version_id, b_node_key); + +create index ix_wf_node_type + on dbo.wf_node (b_version_id, b_node_type, b_xh, b_node_key); +``` + +`b_approval_mode` 取值:`single` 单人、`any` 或签、`all` 会签、`serial` 串行会签、`n_of_m` N 人通过、`percentage` 按比例通过。`requiredCount` 只对 `n_of_m` 生效,`requiredRatio` 只对 `percentage` 生效。 + +### 16.5 节点连线 + +```sql +-- 节点连线表(含条件分支) +create table dbo.wf_edge ( + b_id varchar(50) not null primary key, -- 连线 ID + b_version_id varchar(50) not null, -- 流程版本 + b_from_node_key varchar(80) not null, -- 来源节点编码 + b_to_node_key varchar(80) not null, -- 目标节点编码 + b_condition_json nvarchar(max) null, -- 连线条件 AST + b_priority int not null default 0, -- 判断优先级(小的先判断) + b_canuse tinyint not null default 1, -- 是否启用(0/1) + b_bz nvarchar(1000) null -- 备注 +); + +create index ix_wf_edge_from + on dbo.wf_edge (b_version_id, b_from_node_key, b_canuse, b_priority, b_to_node_key); + +create index ix_wf_edge_to + on dbo.wf_edge (b_version_id, b_to_node_key, b_canuse, b_priority, b_from_node_key); +``` + +同一来源节点的条件连线按 `b_priority` 从小到大判断,条件分支必须有默认出口,避免条件都不满足时流程卡死。 + +### 16.6 审批人规则 + +```sql +-- 审批人规则表(节点内审批人、抄送人的解析规则) +create table dbo.wf_actor_rule ( + b_id varchar(50) not null primary key, -- 规则 ID + b_version_id varchar(50) not null, -- 流程版本 + b_node_key varchar(80) not null, -- 节点编码 + b_actor_type varchar(30) not null, -- 解析类型:user(指定用户)/initiator(发起人)/initiator_manager(发起人直属领导)/dept_manager(部门负责人)/position(岗位)/org_role(组织角色)/field_user(业务字段用户)/custom(自定义解析器) + b_actor_ref varchar(150) null, -- 用户、岗位、角色或字段编码 + b_resolve_json nvarchar(max) null, -- 解析参数 + b_condition_json nvarchar(max) null, -- 规则适用条件 AST + b_fallback_type varchar(30) null, -- 解析为空时的兜底:none(不兜底)/admin(管理员)/initiator(发起人)/custom(自定义) + b_fallback_ref varchar(150) null, -- 兜底引用 + b_priority int not null default 0, -- 优先级(串行会签的顺序来源) + b_canuse tinyint not null default 1, -- 是否启用(0/1) + b_bz nvarchar(1000) null -- 备注 +); + +create index ix_wf_actor_rule_node + on dbo.wf_actor_rule (b_version_id, b_node_key, b_canuse, b_priority, b_actor_type); +``` + +解析结果必须把规则快照和实际用户快照写入运行表,组织架构后续变化不影响历史审批责任。 + +### 16.7 流程实例 + +```sql +-- 流程实例表(某张业务单据某次提交的运行流程) +create table dbo.wf_instance ( + b_id uniqueidentifier not null primary key, -- 实例 ID(应用层生成 UUIDv7) + b_definition_id varchar(50) not null, -- 流程定义编码 + b_version_id varchar(50) not null, -- 流程版本(提交时固定) + b_binding_id varchar(50) not null, -- 命中的绑定(wf_binding.b_id) + b_module_id varchar(50) not null, -- 业务模块编码 + b_event varchar(30) not null, -- 业务事件:submit(送审)/resubmit(重新提交)/change(变更) + b_business_id varchar(50) not null, -- 业务主键(统一按字符串保存) + b_business_no nvarchar(200) null, -- 业务单号(列表展示) + b_org_id varchar(50) null, -- 公司/组织编码 + b_initiator_id varchar(50) not null, -- 发起人(用户编码) + b_initiator_dept_id varchar(50) null, -- 发起人部门编码 + b_status varchar(30) not null, -- 实例状态:running(运行中)/approved(已通过)/rejected(已驳回)/returned(已退回)/withdrawn(已撤回)/cancelled(已取消)/expired(已过期)/error(异常待处理) + b_context_json nvarchar(max) null, -- 运行变量 + b_snapshot_json nvarchar(max) null, -- 提交时业务快照 + b_current_summary_json nvarchar(max) null, -- 列表展示用当前节点摘要 + b_row_version bigint not null default 0, -- 乐观锁版本号 + b_idempotency_key varchar(100) null, -- 幂等键 + b_started_at datetime2 not null, -- 开始时间 + b_completed_at datetime2 null, -- 结束时间 + b_created_by varchar(50) null, -- 创建人 + b_created_at datetime2 null, -- 创建时间 + b_updated_by varchar(50) null, -- 最后修改人 + b_updated_at datetime2 null -- 最后修改时间 +); + +create index ix_wf_instance_business + on dbo.wf_instance (b_module_id, b_business_id, b_event, b_status, b_id); + +create index ix_wf_instance_initiator + on dbo.wf_instance (b_initiator_id, b_status, b_started_at, b_id); + +create index ix_wf_instance_version + on dbo.wf_instance (b_version_id, b_started_at, b_id); +``` + +“驳回”和“退回”必须区分:驳回结束本次流程,退回允许修改后重新提交。 + +### 16.8 节点运行 + +```sql +-- 节点运行表(节点在实例中的一次进入和处理记录) +create table dbo.wf_node_run ( + b_id uniqueidentifier not null primary key, -- 节点运行 ID(UUIDv7) + b_instance_id uniqueidentifier not null, -- 流程实例(wf_instance.b_id) + b_node_key varchar(80) not null, -- 节点编码 + b_run_no int not null default 1, -- 同一节点的第几次运行(退回、循环用) + b_status varchar(30) not null, -- 节点状态:pending(待激活)/running(运行中)/completed(已完成)/skipped(已跳过) + b_required_count int null, -- 应签人数(按比例时在节点激活时换算) + b_completed_count int not null default 0, -- 已通过人数 + b_rejected_count int not null default 0, -- 已驳回人数 + b_entered_at datetime2 null, -- 进入时间 + b_exited_at datetime2 null, -- 离开时间 + b_actor_snapshot_json nvarchar(max) null, -- 审批人规则快照 + b_created_at datetime2 null -- 创建时间 +); + +create index ix_wf_node_run_instance + on dbo.wf_node_run (b_instance_id, b_status, b_node_key, b_run_no, b_id); +``` + +`b_required_count`、`b_completed_count` 和 `b_rejected_count` 必须由后端在锁定节点运行记录后更新,不能由前端计算。 + +### 16.9 待办任务 + +```sql +-- 待办任务表(一名实际审批人一行) +create table dbo.wf_task ( + b_id uniqueidentifier not null primary key, -- 任务 ID(UUIDv7) + b_instance_id uniqueidentifier not null, -- 流程实例 + b_node_run_id uniqueidentifier not null, -- 节点运行(会签、或签聚合判断用) + b_node_key varchar(80) not null, -- 节点编码 + b_actor_rule_id varchar(50) null, -- 命中的审批人规则 + b_assignee_type varchar(30) not null, -- 受理人类型:user(指定用户)/position(岗位)/role(角色)/delegate(委托) + b_assignee_id varchar(50) not null, -- 实际用户编码 + b_seq int not null default 0, -- 同节点内处理顺序(串行会签生效) + b_status varchar(30) not null, -- 任务状态:waiting(未激活)/pending(待处理)/claimed(已抢占)/approved(已通过)/rejected(已驳回)/cancelled(已取消)/expired(已过期) + b_due_at datetime2 null, -- 应处理时间 + b_claimed_by varchar(50) null, -- 抢占人 + b_claimed_at datetime2 null, -- 抢占时间 + b_completed_by varchar(50) null, -- 处理人 + b_completed_at datetime2 null, -- 处理时间 + b_rule_snapshot_json nvarchar(max) null, -- 审批人解析快照 + b_created_at datetime2 null, -- 创建时间 + b_updated_at datetime2 null -- 最后修改时间 +); + +create index ix_wf_task_assignee + on dbo.wf_task (b_assignee_id, b_status, b_due_at, b_id); + +create index ix_wf_task_instance + on dbo.wf_task (b_instance_id, b_status, b_node_run_id, b_id); + +create index ix_wf_task_due + on dbo.wf_task (b_status, b_due_at, b_id); +``` + +会签、或签和串行会签通过同一个 `b_node_run_id` 聚合判断;串行会签的未激活任务使用 `waiting` 状态,只有 `pending` 任务出现在待办列表。 + +### 16.10 审批动作 + +```sql +-- 审批动作表(只追加,不更新历史动作) +create table dbo.wf_action ( + b_id uniqueidentifier not null primary key, -- 动作 ID(UUIDv7) + b_instance_id uniqueidentifier not null, -- 流程实例 + b_node_run_id uniqueidentifier null, -- 节点运行 + b_task_id uniqueidentifier null, -- 待办任务 + b_action varchar(30) not null, -- 动作编码:submit(送审)/approve(通过)/reject(驳回)/return(退回)/withdraw(撤回)/cancel(取消)/claim(抢占)/transfer(转交)/delegate(委托)/reassign(转派)/add_approver(加签)/urge(催办)/cc(抄送) + b_operator_id varchar(50) null, -- 操作人(用户编码,系统动作为空) + b_from_status varchar(30) null, -- 变更前状态 + b_to_status varchar(30) null, -- 变更后状态 + b_comment nvarchar(2000) null, -- 审批意见 + b_payload_json nvarchar(max) null, -- 动作载荷(命中的条件、边、加签位置等) + b_request_id varchar(64) null, -- 请求 ID + b_trace_id varchar(64) null, -- 链路 ID + b_occurdatetime datetime2 not null -- 发生时间 +); + +create index ix_wf_action_instance + on dbo.wf_action (b_instance_id, b_occurdatetime, b_id); + +create index ix_wf_action_operator + on dbo.wf_action (b_operator_id, b_occurdatetime, b_id); +``` + +撤回、转交、加签和管理员转派也必须形成动作记录,历史动作只追加不修改。 + +### 16.11 抄送记录 + +```sql +-- 抄送记录表(抄送人不产生待办,不参与节点判定) +create table dbo.wf_cc ( + b_id uniqueidentifier not null primary key, -- 抄送 ID(UUIDv7) + b_instance_id uniqueidentifier not null, -- 流程实例 + b_node_run_id uniqueidentifier null, -- 来源节点运行 + b_node_key varchar(80) null, -- 来源节点编码 + b_recipient_id varchar(50) not null, -- 抄送接收人(用户编码) + b_actor_rule_id varchar(50) null, -- 命中的抄送规则 + b_status varchar(20) not null default 'unread', -- 阅读状态:unread(未读)/read(已读) + b_read_at datetime2 null, -- 阅读时间 + b_rule_snapshot_json nvarchar(max) null, -- 抄送人解析快照 + b_created_at datetime2 null -- 创建时间 +); + +create index ix_wf_cc_recipient + on dbo.wf_cc (b_recipient_id, b_status, b_created_at, b_id); + +create index ix_wf_cc_instance + on dbo.wf_cc (b_instance_id, b_created_at, b_id); +``` + +### 16.12 委托 + +```sql +-- 委托表(审批人把待办委托给他人处理) +create table dbo.wf_delegation ( + b_id varchar(50) not null primary key, -- 委托 ID + b_from_user_id varchar(50) not null, -- 委托人(用户编码) + b_to_user_id varchar(50) not null, -- 受托人(用户编码) + b_scope_json nvarchar(max) null, -- 委托范围(模块、动作、组织) + b_start_at datetime2 not null, -- 生效时间 + b_end_at datetime2 not null, -- 失效时间 + b_reason nvarchar(500) null, -- 委托原因 + b_canuse tinyint not null default 1, -- 是否启用(0/1) + b_created_by varchar(50) null, -- 创建人 + b_created_at datetime2 null, -- 创建时间 + b_updated_by varchar(50) null, -- 最后修改人 + b_updated_at datetime2 null -- 最后修改时间 +); + +create index ix_wf_delegation_user + on dbo.wf_delegation (b_from_user_id, b_canuse, b_start_at, b_end_at, b_to_user_id); +``` + +### 16.13 通知消息 + +```sql +-- 通知消息表(outbox,异步发送,不阻塞审批事务) +create table dbo.wf_outbox ( + b_id uniqueidentifier not null primary key, -- 消息 ID(UUIDv7) + b_event_code varchar(80) not null, -- 事件编码 + b_aggregate_type varchar(30) not null, -- 聚合类型:instance(流程实例)/task(待办任务) + b_aggregate_id varchar(50) not null, -- 聚合 ID + b_payload_json nvarchar(max) not null, -- 消息载荷 + b_status varchar(20) not null default 'pending', -- 发送状态:pending(待发送)/sent(已发送)/failed(发送失败) + b_retry_count int not null default 0, -- 重试次数 + b_next_retry_at datetime2 null, -- 下次重试时间 + b_last_error nvarchar(2000) null, -- 最后错误 + b_occurdatetime datetime2 not null -- 发生时间 +); + +create index ix_wf_outbox_dispatch + on dbo.wf_outbox (b_status, b_next_retry_at, b_occurdatetime, b_id); +``` + +实例、任务、抄送记录和业务状态在同一数据库事务中提交;通知发送失败不回滚审批事务,超过阈值进入人工处理队列。 diff --git a/code/fms/FMS模块关系与权限边界重构计划.md b/code/fms/FMS模块关系与权限边界重构计划.md deleted file mode 100644 index d416e8b0..00000000 --- a/code/fms/FMS模块关系与权限边界重构计划.md +++ /dev/null @@ -1,547 +0,0 @@ -# FMS 模块关系与权限边界重构计划 - -> 状态:草案,供评审 -> -> 目标:明确模块树、菜单绑定、数据关系、权限和删除策略的边界,降低“模块关联”配置的理解成本,避免一个关系表同时承担多种语义。 - -## 一、结论先行 - -“模块关联”不应该同时承担菜单授权、模块父子关系、业务数据关系和删除规则。 - -本次计划采用以下分工: - -| 问题 | 权威数据 | 说明 | -| --- | --- | --- | -| 模块业务组织层级 | `s_module.b_parent_id` | 表达业务域、模块分组和权限上下文边界 | -| 菜单导航层级 | `s_menu.b_parent_id` | 表达目录、页面和外链的导航树 | -| 菜单使用哪些模块 | `s_menu_module` | 菜单与模块的多对多绑定,保存明确的模块清单 | -| 用户拥有哪些权限 | `s_power`、`s_user_power` | 模块访问、业务动作和菜单入口权限 | -| 数据字段之间的引用关系 | `s_relation` | 只表达模块字段之间的结构关系 | -| 删除前的业务限制 | `s_delete_rule` | 用只读 SQL 检查状态、单据和跨表条件 | -| 关联记录如何处置 | 删除策略 | `restrict`、`cascade`、`none`,不参与菜单权限计算 | - -因此: - -1. **菜单权限不需要依赖 `s_relation`。** -2. **模块父子关系使用 `s_module.b_parent_id`,不从字段关系推导。** -3. **`s_relation` 建议保留为“数据关系”,但不再称为菜单权限意义上的模块关联。** -4. **删除策略继续存在,但从数据关系的菜单中剥离,统一放到删除策略入口。** - -当前设计文档已经有这几张表的雏形:模块树使用 `s_module.b_parent_id`,菜单绑定使用 `s_menu_module`,数据关系使用 `s_relation`。[核心表设计](FMS新系统核心表结构设计.md) [模块设计评审](FMS模块设计评审.md) - -## 二、当前问题 - -### 2.1 “模块关联”包含了三种不同关系 - -目前容易把下面三件事都叫作模块关联: - -1. **组织关系**:海运模块下面有主单、箱、费用模块。 -2. **数据关系**:箱表的 `order_id` 引用主单表的 `id`。 -3. **授权关系**:某个菜单页面使用主单模块和箱模块,用户可以进入并操作这些模块。 - -它们的数学结构和运行规则不同: - -| 关系 | 结构 | 典型约束 | -| --- | --- | --- | -| 模块组织 | 树 | 一个父节点、不能循环 | -| 数据关系 | 图 | 可以多对多、可以有多个入边 | -| 菜单绑定 | 多对多 | 一个页面可用多个模块,一个模块可被多个页面使用 | -| 权限授权 | 用户到权限点 | 不能因为数据引用就自动获得权限 | - -如果用数据关系建立模块父子树,会遇到多个父节点、循环和跨业务域引用。例如“费用 → 客户”是数据引用,但费用模块并不是客户模块的子模块。把它用于权限继承会造成权限扩大。 - -### 2.2 删除策略和菜单权限没有共同语义 - -删除策略回答: - -> 删除一条业务记录时,关联记录如何处理?什么条件下不允许删除? - -权限回答: - -> 用户能否进入页面、使用模块、执行动作、查看字段和查看哪些数据? - -两者的计算入口、管理员和风险都不同。把删除配置放进菜单权限关联,会让配置人员误以为“菜单绑定”会影响删除,也会让关系面板出现过多不相关列。 - -### 2.3 当前代码已经有两个真实消费者 - -当前前端菜单管理使用 `s_menu_module` 保存菜单与模块的绑定;模块关系面板维护 `s_relation`。后端 `DataDeleteService` 会读取启用的 `s_relation`,展开 `cascade` 并检查 `restrict`。 - -这说明 `s_relation` 不是完全没有价值,但它的价值来自**数据删除和数据影响分析**,不是菜单授权。若以后改成每个模块完全由专用 SQL 删除,才可以进一步评估是否移除它。 - -## 三、目标模型 - -### 3.1 三棵树、两类关系 - -```text -模块组织树 -s_module.b_parent_id -海运 -├── 海运主单 -├── 箱信息 -└── 费用 - -菜单导航树 -s_menu.b_parent_id -业务管理 -└── 海运工作台 - -菜单与模块绑定 -s_menu_module -海运工作台 ── 海运主单 -海运工作台 ── 箱信息 -海运工作台 ── 费用 - -数据字段关系 -s_relation -箱信息.order_id ──> 海运主单.id -费用.order_id ──> 海运主单.id -``` - -模块树只表达业务组织。菜单树只表达导航。菜单页面使用哪些数据模块由 `s_menu_module` 明确保存。数据字段关系只用于数据层,不参与菜单树和权限树。 - -### 3.2 权限归属:模块为主,菜单为入口 - -权限应当**跟着模块**,菜单只负责提供入口。 - -```text -用户 - └── 拥有权限点 - ├── menu.* → 能否进入菜单页面 - ├── module.* → 能否使用模块 - └── action.* → 能否执行模块动作 - -菜单页面 - └── s_menu_module → 页面使用哪些模块 - -模块 - ├── s_user_field_power → 字段能力 - └── s_user_data_power → 数据范围 -``` - -菜单绑定模块不等于授予模块权限。原因有三点: - -1. 一个模块可能被多个菜单复用,权限应该保持一致; -2. 一个菜单可能使用主模块、明细模块和多个辅助模块,不适合把它们的权限合并成一个菜单权限; -3. 后端接口、导出、保存和业务动作可能绕过菜单入口,必须仍按模块权限和动作权限校验。 - -因此运行时按以下顺序判断: - -1. 用户是否拥有当前菜单的 `menu.*` 权限; -2. 当前页面所需模块是否拥有 `module.*` 权限; -3. 当前请求的操作是否拥有对应的 `action.*` 或标准操作权限; -4. 查询和写入时继续追加字段权限和数据范围条件。 - -如果一个页面绑定多个模块,菜单权限只决定能否进入页面,每个模块仍单独判断。主模块或必需模块无权限时,页面应隐藏或拒绝进入;可选模块无权限时,可以隐藏对应面板。是否为“必需模块”属于页面配置,不由 `s_relation` 推导。 - -模块父子树可以作为管理员批量授权的操作范围,但第一期不建议运行时隐式继承:勾选父模块时可以在界面展开子模块,最终保存每个数据模块的明确授权行。这样新建子模块不会在用户不知情的情况下自动获得所有权限。 - -### 3.3 权限计算边界 - -权限计算分为五层: - -1. `menu.*`:能否看到和进入菜单页面。 -2. `module.*`:能否使用页面中的某个模块。 -3. `action.*`:能否执行审核、结算等业务动作。 -4. `s_user_field_power`:字段查看、编辑、查询和导出权限。 -5. `s_user_data_power`:数据行范围。 - -菜单绑定多个模块时,不合并这些模块的权限;每个模块仍按自己的模块编码判断。数据关系也不自动传播权限。若管理员在界面上勾选一个模块分类节点,可以自动展开下级数据模块,但落库仍保存明确的模块权限或菜单绑定行。 - -### 3.4 用户授权页面如何呈现 - -管理员不需要直接理解 `s_relation`。授权页面以业务域和模块树呈现: - -```text -海运(业务域,只做分组) -├── 海运主单(data) -├── 装箱信息(data) -└── 费用(data) -``` - -建议将授权页面分成三个视图: - -#### 菜单入口 - -左侧显示菜单树,管理员选择“海运工作台”等页面,保存 `menu.*` 权限。 - -#### 模块能力 - -显示当前页面绑定的模块,以及完整的业务模块树。每个 `data` / `virtual` 模块显示: - -- 是否可访问; -- 标准操作:查看、新增、编辑、删除、导出; -- 已配置的业务动作,例如审核、结算; -- 当前模块是否被哪些菜单使用。 - -选择“海运”父节点时,界面可以全选或取消全部子模块,并使用半选状态表示部分授权。这个操作只是批量生成明确的模块授权行,不保存“沿关系自动继承”的隐式规则。 - -#### 字段和数据范围 - -管理员先选择一个具体模块,再配置字段权限和数据范围。不要在“海运”父节点上混合展示主单、装箱和费用的字段,因为这些字段属于不同模块,数据范围也可能不同。 - -页面绑定关系可以提供一个“按页面补齐模块权限”的快捷操作:管理员选择菜单后,系统列出该页面使用的主模块、明细模块和辅助模块,管理员确认后一次性生成模块权限。这个操作是显式的授权动作,菜单绑定本身不自动授权。 - -页面确实依赖某个模块时,应在页面配置中标记该模块为必需模块;可选模块则在用户没有权限时隐藏对应面板。必需/可选属于页面使用配置,不从 `s_relation` 的数据关系推导。 - -授权效果示例: - -| 用户 | 菜单入口 | 模块权限 | 页面效果 | -| --- | --- | --- | --- | -| 张三 | 海运工作台 | 主单、装箱 | 能进入页面,费用面板隐藏或不可用 | -| 李四 | 海运工作台 | 主单、装箱、费用 | 能使用完整页面 | -| 王五 | 无海运菜单 | 主单 | 不能从海运菜单进入,但模块接口仍按模块权限统一校验 | - -页面是否显示可以由“菜单权限 + 至少一个可访问的必需模块”共同决定;真正的查询、保存、删除和动作权限仍必须在后端按模块执行。 - -### 3.5 默认使用“业务授权方案”,隐藏技术细节 - -如果每个用户都要先分配菜单,再逐个分配主单、装箱、费用和动作,业务管理员会觉得权限系统过于复杂。因此默认授权入口不直接展示三套独立权限表,而是展示业务人员能理解的授权方案: - -```text -海运 -├── 未授权 -├── 只读 -├── 经办 -└── 主管 -``` - -方案的含义由系统管理员预先定义。例如: - -| 方案 | 菜单入口 | 海运主单 | 装箱信息 | 费用 | -| --- | --- | --- | --- | --- | -| 只读 | 海运工作台、海运单列表 | 查看、导出 | 查看 | 查看 | -| 经办 | 海运工作台、海运单列表 | 查看、新增、编辑、导出 | 查看、编辑 | 查看 | -| 主管 | 海运全部相关页面 | 全部标准操作和业务动作 | 全部标准操作 | 查看、编辑、导出、结算 | - -管理员选择“海运·经办”后,界面先展示授权预览: - -```text -将授予: -菜单:海运工作台、海运单列表 -模块:海运主单(查看/新增/编辑/导出) - 装箱信息(查看/编辑) - 费用(查看) -``` - -确认后,在同一事务中写入菜单权限和模块/动作权限。这里的“方案”只是管理员操作的简化入口,运行时的真实权限仍然按模块、动作、字段和数据范围校验;菜单绑定不会在运行时隐式授予模块权限。 - -默认页面只展示业务名称和方案,不展示模块编码、字段编码和 `s_relation`。授权预览中的“主单、装箱、费用”依赖来自 `s_menu_module` 的页面模块清单,不从数据关系推导。 - -高级管理员可以展开“详细调整”:单独修改某个模块、动作、字段或数据范围。详细调整生成同一套底层权限记录,不另建一套与方案互相覆盖的规则。方案变更后,应显示变更前后差异并要求确认。 - -第一期不必新增复杂的角色继承体系。可以先把方案作为权限页面上的预置操作,直接物化为现有 `s_power` / `s_user_power` 等权限行。出现多个用户需要相同授权时,再把方案独立为可复用的权限模板。 - -## 四、目标表设计 - -### 4.1 `s_module`:模块组织树 - -继续使用现有字段: - -- `b_id`:模块编码。 -- `b_parent_id`:直接父模块,树关系的唯一权威来源。 -- `b_depth`、`b_path`:由业务层维护的派生字段。 -- `b_module_type`:`module`、`data`、`virtual`。 - -约束: - -- `module` 可以包含下级 `module`、`data` 和 `virtual`。 -- `data`、`virtual` 默认作为叶子节点。 -- 不允许自指和循环。 -- 移动模块时,在同一事务内更新当前节点及全部后代的 `b_depth`、`b_path`。 -- 不从 `s_relation` 推导 `b_parent_id`。 - -### 4.2 `s_menu`、`s_menu_module`:菜单与模块绑定 - -`s_menu` 继续使用 `b_parent_id` 建立菜单树。 - -`s_menu_module` 继续使用联合主键: - -```text -(b_menu_id, b_module_id) -``` - -绑定规则: - -- `directory`、`external` 不强制绑定模块。 -- `page` 可以绑定一个或多个 `data` / `virtual` 模块。 -- `module` 类型只作为分类和上下文,不直接进入菜单模块绑定。 -- UI 可以按模块树批量勾选,但保存时写入明确的 `s_menu_module` 行。 -- 删除模块前检查并清理其菜单绑定,不能通过数据关系级联推断菜单绑定。 - -### 4.3 `s_relation`:数据关系 - -建议将管理页面名称改为“数据关系”,表继续使用字段级复合键: - -```text -b_source_module_id -b_source_field -b_target_module_id -b_target_field -b_relation_type -- one_to_one / one_to_many / many_to_one -b_canuse -b_xh -``` - -它只回答: - -> 哪个模块的哪个字段与另一个模块的哪个字段存在结构关系? - -它不保存: - -- 菜单编码。 -- 用户编码。 -- 权限编码。 -- 模块树父节点。 -- 菜单显示顺序。 - -#### 删除行为的处理 - -当前通用删除服务已经依赖 `b_on_delete`,因此建议采用兼容方案: - -- 数据库中暂时保留 `s_relation.b_on_delete`。 -- 关系面板只显示该列或提供跳转,不作为菜单权限关系编辑。 -- 唯一可编辑入口放到“删除策略 → 关联删除处理”。 -- 只保留 `restrict`、`cascade`、`none` 三值。 -- 不根据 `b_relation_type` 自动推断删除行为。 - -如果后续确认所有模块都由专用 SQL 或存储过程删除,则可以迁移掉 `b_on_delete`,让 `s_relation` 只保留结构关系。 - -### 4.4 `s_delete_rule`:业务删除条件 - -`s_delete_rule` 只处理“什么情况下不允许删除”,不再保存数据关系键: - -```text -b_id -b_module_id -b_sql -- 只读检查 SQL,命中行即拒绝删除 -b_message -b_xh -b_canuse -``` - -适合配置: - -- 已审核单据不能删除。 -- 已开票记录不能删除。 -- 已生成下游业务单据不能删除。 -- 状态或跨表条件不满足时不能删除。 - -SQL 只做检查,不允许配置 SQL 直接修改或删除业务数据。实际物理删除继续由后端事务服务负责。 - -## 五、前端改造方案 - -### 5.1 模块管理页面 - -建议将业务能力分节调整为: - -```text -基础 -字段 -界面 -数据关系 -删除策略 -自动编码 -权限 -多语言 -``` - -其中: - -- “数据关系”只维护字段关系。 -- “删除策略”包含“关联删除处理”和“删除 SQL 规则”。 -- “权限”只维护 `s_power`,不展示删除配置。 -- “数据关系”只对 `data` 模块开放。 - -### 5.2 数据关系面板 - -保留: - -- 新增关系。 -- 源字段选择。 -- 目标模块、目标字段选择。 -- 关系类型。 -- 启用/停用。 -- 排序号。 -- 当前模块作为源的关系编辑。 -- 其他模块指向当前模块的关系只读展示。 - -调整: - -- 面板标题从“模块关联”改为“数据关系”。 -- 删除处理列改为只读并提供“前往删除策略”的入口,或者完全移入删除策略面板。 -- 不出现菜单、权限、用户等字段。 -- 保存前校验模块、字段存在,字段类型可匹配,关系不能重复。 - -### 5.3 菜单管理页面 - -- 左侧继续展示模块树。 -- 勾选 `module` 分类节点时,展开其下的 `data` / `virtual` 模块。 -- 保存时只提交 `s_menu_module` 的增删行。 -- 菜单页面绑定的模块不反向修改模块树。 -- 模块被多个菜单使用时,只维护多条菜单绑定,不复制模块。 - -### 5.4 权限面板 - -- 模块使用权限继续以 `module.{moduleCode}` 为主。 -- 业务动作使用 `action.{moduleCode}.{action}`。 -- 菜单权限只控制入口。 -- 不因 `s_relation` 存在而自动给用户增加目标模块权限。 -- 如果未来需要继承,只允许从 `s_module.b_parent_id` 的组织树计算,并明确配置继承边界;不使用字段关系推导。 - -## 六、删除实现取舍 - -### 6.1 当前阶段推荐方案 - -保留通用删除服务: - -```text -模块删除 -→ 根据 s_relation.b_on_delete 展开 cascade -→ 检查 restrict 外部引用 -→ 执行 s_delete_rule SQL 检查 -→ 同一事务内物理删除 -``` - -这样适合模块数量较多、希望新增模块主要靠配置的场景。`s_relation` 为删除服务提供结构信息,`s_delete_rule` 为业务条件提供 SQL 出口,两者职责明确。 - -### 6.2 SQL 直接处理的适用边界 - -SQL 适合: - -- 复杂查询和跨表判断。 -- 固定模块的特殊清理逻辑。 -- 数据库外键的 `CASCADE` / `NO ACTION`(物理结构稳定时)。 - -不建议: - -- 让管理员配置任意 `DELETE SQL` 作为通用删除入口。 -- 用 SQL 文本判断用户菜单权限。 -- 用 SQL 关系自动推导模块父子和权限继承。 - -如果未来选择“完全模块专用 SQL 删除”,迁移顺序应是:先为每个模块补齐删除服务和测试,再停止通用删除服务读取 `s_relation`,最后删除 `b_on_delete`,不能先删元数据再补删除逻辑。 - -## 七、迁移步骤 - -### 阶段 0:盘点和校验 - -1. 统计现有 `s_relation` 行,确认每行的源模块、源字段、目标模块、目标字段真实存在。 -2. 检查重复关系、自引用、无效模块和无效字段。 -3. 盘点旧系统 `s_modulelink` 或其他模块链接数据,确认哪些是菜单绑定,哪些是数据关系,禁止直接整表迁移到 `s_relation`。 -4. 统计菜单、模块、权限之间的现有绑定,找出没有模块权限但已有菜单入口的情况。 - -### 阶段 1:先改语义和界面 - -1. 将模块管理中的“模块关联”改为“数据关系”。 -2. 移除它对菜单权限的描述。 -3. 在菜单管理中明确展示 `s_menu_module` 绑定。 -4. 增加“删除策略”分组,保证 `b_on_delete` 只有一个编辑入口。 -5. 不立即删除数据库列和旧数据,保持兼容。 - -### 阶段 2:拆分权限计算 - -1. 路由和侧栏只根据菜单权限及菜单绑定判断入口。 -2. 页面内模块操作根据 `s_power` / `s_user_power` 独立判断。 -3. 字段和数据范围继续走独立权限表。 -4. 全局搜索确认权限代码没有读取 `s_relation`。 - -### 阶段 3:收敛删除策略 - -1. 删除前条件统一读取 `s_delete_rule`。 -2. 关联删除统一读取 `s_relation.b_on_delete`。 -3. 删除列表入口和 `saveobjt` 使用同一删除服务。 -4. 对 `cascade` 做环路、深度、重复记录和事务回滚测试。 -5. 对 `restrict` 返回引用模块、字段和示例记录,便于用户处理。 - -### 阶段 4:清理旧概念 - -只有在兼容数据完成迁移后,才删除: - -- 旧的菜单关系字段。 -- 旧的引用规则或重复删除配置。 -- 模块关联面板中的权限文案。 -- 不再使用的关系推导逻辑。 - -## 八、验收标准 - -### 模块树 - -- 模块只能有一个直接父节点。 -- 不能自引用或形成循环。 -- 移动节点后,所有后代的 `b_path`、`b_depth` 正确。 -- 数据字段关系不会改变模块树。 - -### 菜单和权限 - -- 一个菜单可以绑定多个模块。 -- 一个模块可以被多个菜单复用。 -- 勾选模块分类节点只是批量操作,保存后仍是明确绑定行。 -- 只有菜单权限不能直接操作模块数据。 -- 有数据关系的两个模块不会自动互相获得权限。 - -### 数据关系 - -- 关系字段存在且类型可匹配。 -- 关系重复时保存失败。 -- 反向关系可查看,但不会重复提交或误删。 -- `s_relation` 不包含菜单、用户和权限字段。 - -### 删除 - -- `cascade` 能正确删除拥有关系的子记录。 -- `restrict` 能阻止外部引用导致的父记录删除。 -- `none` 不自动处理关联记录,并在配置界面显示风险提示。 -- 删除 SQL 命中时整批回滚。 -- 技术表使用 `target=direct` 时不读取模块关系。 -- 任何检查失败时,不留下部分删除结果。 - -## 九、文件影响范围 - -### 数据库和脚本 - -- `sql/fms_core.sql`:确认 `s_module`、`s_menu`、`s_menu_module`、`s_relation`、`s_power` 的职责注释和字段。 -- `sql/fms_delete_rule.sql`:只保留删除策略迁移和 `s_delete_rule` 初始化。 -- 新增一份关系数据校验/迁移脚本,先报告问题,不直接静默修复。 - -### 前端 - -- `fms-vue/src/views/module/module-management/index.vue`:调整分节名称和数据关系/删除策略的编排。 -- `ModuleRelationPanel.vue`:改名、缩减职责、移除权限语义。 -- `ModuleRulePanel.vue`:作为删除 SQL 规则面板保留。 -- `fms-vue/src/views/module/menu-management/index.vue`:继续以 `s_menu_module` 保存菜单绑定。 -- `stores/permissions.js`:确认权限计算不读取 `s_relation`。 - -### 后端 - -- `DataDeleteService.java`:继续只把 `s_relation` 当作数据关系来源。 -- `SqlPermissionService.java`:权限只读取权限表,不通过数据关系推导。 -- 菜单接口和模块接口分别校验自己的树结构,不能共用一套父子关系处理。 - -## 十、需要确认的唯一决策 - -本计划默认保留通用删除服务,因此保留 `s_relation.b_on_delete` 作为底层删除策略字段,但把编辑入口移到“删除策略”。 - -如果决定所有业务模块都用专用 SQL 或存储过程处理删除,则可以选择另一条路线: - -```text -s_relation 只保留数据结构关系 -s_delete_rule 保留删除前检查 -模块删除由各自服务/存储过程完成 -``` - -这条路线的配置更少,但新增模块不能只靠元数据完成删除行为,需要额外编写和测试模块删除逻辑。 - -在当前 FMS 已经存在通用 `DataDeleteService`、模块数量较多的前提下,建议先采用默认方案:**保留数据关系,拆出权限语义,删除策略单独管理,暂不改成纯 SQL 删除。** - -## 十一、原型演示 - -对应的交互原型位于 [权限分配原型.html](权限分配原型.html),直接用浏览器打开即可查看。 - -原型按常见企业系统的授权路径组织: - -1. 左侧切换用户,顶部查看当前授权来源和保存状态。 -2. 先选择海运、空运或财务业务域。 -3. 默认选择“未授权、只读、经办、主管”等业务方案,右侧立即预览菜单入口、模块和动作。 -4. 需要精细控制时,在海运业务中打开“详细权限调整”,分别配置菜单、模块、字段和数据范围。 -5. 用“预览最终权限”查看后端实际会收到的授权结果,用“撤销修改”验证未保存变更不会直接生效。 - -原型中的角色方案只是降低管理员理解成本的操作入口,底层仍然落成菜单、模块和动作权限;业务域和模块之间的字段关系没有被用来推导权限。 diff --git a/code/fms/FMS模块设计评审.md b/code/fms/FMS模块设计评审.md deleted file mode 100644 index 405a4db2..00000000 --- a/code/fms/FMS模块设计评审.md +++ /dev/null @@ -1,237 +0,0 @@ -# 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`): - -```sql -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`): - -```text -数据结构 → 字段定义 -界面配置 → 表单设计 / 列表配置 / 查询配置 -业务能力 → 自动编码 / 权限 / 模块关联 / 删除规则 -高级设置 → 多语言 -``` - -这是**按「配置的载体」分,而不是按「业务问题」分**。于是同一个业务问题被切到多处: - -- 「这个模块的记录在什么情况下不许删」→ **删除规则** -- 「这条记录被别人引用会怎样」→ **模块关联** -- 「谁能用、能看哪些字段、能看哪些数据」→ **权限** - -而用户在真实场景里问的是一个整体问题:**「这张单据在什么情况下会被拦下来?」** 答案分散在三个面板里。 - -**这正是「删除规则要在模块关联和删除规则两处维护」困惑的根因**——不是删除规则本身的问题,是分节维度的问题。 - -### 建议 - -按**「改这个模块会影响什么」**重新分节: - -```text -基础 → 模块定义(类型、查询来源、保存目标、主键) -字段 → 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 边界。 diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/FmsApiApplication.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/FmsApiApplication.class index 9a055d80..559592fc 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/FmsApiApplication.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/FmsApiApplication.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/AuthProperties.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/AuthProperties.class index a5187a9b..22cfa4af 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/AuthProperties.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/AuthProperties.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Local.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Local.class index 822c502c..5ac35469 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Local.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Local.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Oss.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Oss.class index feb115f3..aba3549e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Oss.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties$Oss.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties.class index 9098305c..7ca39c05 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/FileStorageProperties.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JacksonConfig.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JacksonConfig.class index e57213b4..37408c3f 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JacksonConfig.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JacksonConfig.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JwtAuthFilter.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JwtAuthFilter.class index 04b4d42c..7f2fc20b 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JwtAuthFilter.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/config/JwtAuthFilter.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/AuthController.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/AuthController.class index 4d749e7b..6c7b72f2 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/AuthController.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/AuthController.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/DataController.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/DataController.class index f26a14f2..463a65fa 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/DataController.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/DataController.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/ExportController.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/ExportController.class index 3b377d45..d2e7ffee 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/ExportController.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/ExportController.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/FileController.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/FileController.class index 5bdd23f5..cd9ccdb5 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/FileController.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/controller/FileController.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgContext.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgContext.class index 05b8fe4f..b441b83e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgContext.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgContext.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class index 01c250f1..73a3db58 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceFactory.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager$PoolEntry.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager$PoolEntry.class index a7514698..c5e35b10 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager$PoolEntry.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager$PoolEntry.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class index c3c80f8d..3c371400 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDataSourceManager.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfig.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfig.class index 4aefdc8f..62e91cad 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfig.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfig.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class index 9120d513..126443e6 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.class index 9c18dc14..cfe4f00c 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseConfigWatcher.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class index 4caaae3e..b5cb3e35 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties$Pool.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class index b88b12ec..6123a420 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgDatabaseProperties.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgRoutingDataSource.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgRoutingDataSource.class index 31944e40..78d12c4f 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgRoutingDataSource.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/database/OrgRoutingDataSource.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/AuthenticationException.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/AuthenticationException.class index e6a0569d..e1560d86 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/AuthenticationException.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/AuthenticationException.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/BusinessException.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/BusinessException.class index 13e0bdd3..94af84fa 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/BusinessException.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/BusinessException.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.class index 6dc0382c..0eea0492 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/GlobalExceptionHandler.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/SaveObjectException.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/SaveObjectException.class index b1e39134..e523a194 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/SaveObjectException.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/exception/SaveObjectException.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry$AccountKey.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry$AccountKey.class index 3ce03cc9..6b62983b 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry$AccountKey.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry$AccountKey.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry.class index e2690636..a1b9dab4 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ActiveSessionRegistry.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/AuthService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/AuthService.class index 653f5ecd..46d3a22b 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/AuthService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/AuthService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeFormat.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeFormat.class index 415adb7b..7623725e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeFormat.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeFormat.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeRule.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeRule.class index 170e3b14..566b9fb5 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeRule.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$AutoCodeRule.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$CodeCandidate.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$CodeCandidate.class index b2d557df..e7089c2e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$CodeCandidate.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService$CodeCandidate.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService.class index 7344de39..b60ea273 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataSaveService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataService.class index 631a4e27..28c8b8c9 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/DataService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportHeadBuilder.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportHeadBuilder.class index 75cc612f..a648b5bb 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportHeadBuilder.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportHeadBuilder.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportColumn.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportColumn.class index 003cae85..8bba2f7c 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportColumn.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportColumn.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportNode.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportNode.class index 3bda96ff..3ec5b7ad 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportNode.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportNode.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportSchema.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportSchema.class index b56a010c..a5847163 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportSchema.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService$ExportSchema.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService.class index e794b365..e9a43030 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportSchemaService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$1.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$1.class index a1f72a65..6aafb433 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$1.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$1.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ExportRequest.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ExportRequest.class index d679dcc8..7f568c97 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ExportRequest.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ExportRequest.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$PreparedExport.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$PreparedExport.class index 40ab135c..30b1e5ca 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$PreparedExport.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$PreparedExport.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ResultSetRowSource.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ResultSetRowSource.class index d7af75ed..d3179b54 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ResultSetRowSource.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService$ResultSetRowSource.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService.class index 2cdb18e8..f2bc600b 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportStyleHandler.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportStyleHandler.class index d93ba655..d5977c54 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportStyleHandler.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportStyleHandler.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter$RowSource.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter$RowSource.class index 0addead4..e77d747e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter$RowSource.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter$RowSource.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter.class index 87b993d5..98ad2247 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/ExportWorkbookWriter.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/FileService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/FileService.class index c93d6300..881c36f7 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/FileService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/FileService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class index 1fb10021..24b1e7d2 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/SqlPermissionService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/SqlPermissionService.class index 38d3fc21..b8dffcde 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/SqlPermissionService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/SqlPermissionService.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ApiResponse.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ApiResponse.class index 2a5766aa..f0537e84 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ApiResponse.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ApiResponse.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfo.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfo.class index 178049c4..0ad5d9dd 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfo.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfo.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfoExtractor.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfoExtractor.class index 334490a1..fc329abc 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfoExtractor.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ClientInfoExtractor.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$ColumnMetadata.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$ColumnMetadata.class index 4d0c2298..319f068e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$ColumnMetadata.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$ColumnMetadata.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$PageResult.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$PageResult.class index e47bf3ef..4b450936 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$PageResult.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$PageResult.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$TableMetadata.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$TableMetadata.class index d64adc0c..4ce69645 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$TableMetadata.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils$TableMetadata.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils.class index 669b7f6d..0e8c2ddb 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/DbUtils.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpRegionSearcher.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpRegionSearcher.class index e1bc47f1..23cd6142 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpRegionSearcher.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpRegionSearcher.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpUtils.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpUtils.class index 8e837371..f20a9e5e 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpUtils.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/IpUtils.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils$TokenClaims.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils$TokenClaims.class index 752775dc..e8ad6365 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils$TokenClaims.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils$TokenClaims.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils.class index f2ae6fa9..6b2812aa 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/JwtUtils.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ParamUtils.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ParamUtils.class index 779e1855..12d7c118 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ParamUtils.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/ParamUtils.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/StringUtils.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/StringUtils.class index 4b26a3bd..b485b135 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/StringUtils.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/StringUtils.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/UuidV7Generator.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/UuidV7Generator.class index df2a56e9..aeec4597 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/UuidV7Generator.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/UuidV7Generator.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorException.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorException.class index 81bfb7ed..39db6908 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorException.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorException.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorOptions.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorOptions.class index d7f0b038..f67aad8a 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorOptions.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/IdGeneratorOptions.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/OverCostActionArg.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/OverCostActionArg.class index b35764b1..c6f83b73 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/OverCostActionArg.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/contract/OverCostActionArg.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM1.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM1.class index aed62c84..06a6e877 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM1.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM1.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM2.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM2.class index 40d4112c..0d4a11dc 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM2.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/core/SnowWorkerM2.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/DefaultIdGenerator.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/DefaultIdGenerator.class index 81ba7abd..3679d9f8 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/DefaultIdGenerator.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/DefaultIdGenerator.class differ diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/IdGenerator.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/IdGenerator.class index 9ca7e76a..a289499c 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/IdGenerator.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/utils/snowflake/idgen/IdGenerator.class differ diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/AuthServiceTests.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/AuthServiceTests.class index 6ab19287..622e41a8 100644 Binary files a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/AuthServiceTests.class and b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/AuthServiceTests.class differ diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests$JdbcFixture.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests$JdbcFixture.class index 4565f29a..f561651b 100644 Binary files a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests$JdbcFixture.class and b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests$JdbcFixture.class differ diff --git a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests.class b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests.class index 5cf40135..c6ca5921 100644 Binary files a/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests.class and b/code/fms/fms-api/target/test-classes/cn/g3soft/fmsapi/service/DataSaveServiceTests.class differ diff --git a/code/fms/fms-vue/package.json b/code/fms/fms-vue/package.json index 5341dc6a..0db48f4a 100644 --- a/code/fms/fms-vue/package.json +++ b/code/fms/fms-vue/package.json @@ -20,6 +20,8 @@ }, "dependencies": { "@fontsource/noto-sans-sc": "^5.3.0", + "@logicflow/core": "^2.2.5", + "@logicflow/extension": "^2.3.1", "@lucide/vue": "^1.31.0", "axios": "^1.19.0", "dayjs": "^1.11.21", diff --git a/code/fms/fms-vue/pnpm-lock.yaml b/code/fms/fms-vue/pnpm-lock.yaml index 585df71c..b2287cc9 100644 --- a/code/fms/fms-vue/pnpm-lock.yaml +++ b/code/fms/fms-vue/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@fontsource/noto-sans-sc': specifier: ^5.3.0 version: 5.3.0 + '@logicflow/core': + specifier: ^2.2.5 + version: 2.2.5 + '@logicflow/extension': + specifier: ^2.3.1 + version: 2.3.1(@logicflow/core@2.2.5) '@lucide/vue': specifier: ^1.31.0 version: 1.31.0(vue@3.5.41) @@ -81,6 +87,9 @@ importers: packages: + '@antv/hierarchy@0.6.14': + resolution: {integrity: sha512-V3uknf7bhynOqQDw2sg+9r9DwZ9pc6k/EcqyTFdfXB1+ydr7urisP0MipIuimucvQKN+Qkd+d6w601r1UIroqQ==} + '@asamuzakjp/css-color@6.0.7': resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} engines: {node: ^22.13.0 || >=24.0.0} @@ -203,6 +212,14 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@logicflow/core@2.2.5': + resolution: {integrity: sha512-EO5oL67dJIW9LkyZldemxV2ZRKHIZ31hi0n9z3mditxJflSQ7iMzG5HVeiMrlNvgotI6yH0W5hnVOOmkm3QavA==} + + '@logicflow/extension@2.3.1': + resolution: {integrity: sha512-ml75ezT4oSs1W7arf/5uNNNPGkTsLyani8dijh+rOnXHao6P1w30xHwrO6ZeTF7mo/Lk/pJ/Qnqm4+2L5mPeCQ==} + peerDependencies: + '@logicflow/core': ^2.2.5 + '@lucide/vue@1.31.0': resolution: {integrity: sha512-NtjEHhcAa7umPh40wrRmlESJqNGdnpc7LziBKFnW3fmlrW0g2xMCDpdWU7WeEHRSl3xOpxbqiFTLKxoK3CoVCg==} peerDependencies: @@ -637,6 +654,9 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@sphinxxxx/color-conversion@2.2.2': + resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -829,6 +849,9 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1022,6 +1045,9 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hoist-non-react-statics@2.5.5: + resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -1183,6 +1209,9 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1211,6 +1240,9 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + medium-editor@5.23.3: + resolution: {integrity: sha512-he9/TdjX8f8MGdXGfCs8AllrYnqXJJvjNkDKmPg3aPW/uoIrlRqtkFthrwvmd+u4QyzEiadhCCM0EwTiRdUCJw==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1230,6 +1262,23 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mobx-preact@3.0.0: + resolution: {integrity: sha512-ijan/cBs3WmRye87E5+3JmoFBB00KDAwNA3pm7bMwYLPHBAXlN86aC3gdrXw8aKzM5RI8V3a993PphzPv6P4FA==} + peerDependencies: + mobx: 5.x + preact: '>=8' + + mobx-utils@5.6.2: + resolution: {integrity: sha512-a/WlXyGkp6F12b01sTarENpxbmlRgPHFyR1Xv2bsSjQBm5dcOtd16ONb40/vOqck8L99NHpI+C9MXQ+SZ8f+yw==} + peerDependencies: + mobx: ^4.13.1 || ^5.13.1 + + mobx@5.15.7: + resolution: {integrity: sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==} + + mousetrap@1.6.5: + resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1349,6 +1398,14 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + preact@10.29.8: + resolution: {integrity: sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -1363,6 +1420,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + rangy@1.3.2: + resolution: {integrity: sha512-fS1C4MOyk8T+ZJZdLcgrukPWxkyDXa+Hd2Kj+Zg4wIK71yrWgmjzHubzPMY1G+WD9EgGxMp3fIL0zQ1ickmSWA==} + readdirp@5.1.1: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} @@ -1528,6 +1588,14 @@ packages: webpack: optional: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vanilla-picker@2.12.3: + resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==} + vite@8.2.1: resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1701,6 +1769,8 @@ packages: snapshots: + '@antv/hierarchy@0.6.14': {} + '@asamuzakjp/css-color@6.0.7': dependencies: '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -1813,6 +1883,33 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@logicflow/core@2.2.5': + dependencies: + classnames: 2.5.1 + lodash-es: 4.18.1 + mobx: 5.15.7 + mobx-preact: 3.0.0(mobx@5.15.7)(preact@10.29.8) + mobx-utils: 5.6.2(mobx@5.15.7) + mousetrap: 1.6.5 + preact: 10.29.8 + uuid: 9.0.1 + transitivePeerDependencies: + - preact-render-to-string + + '@logicflow/extension@2.3.1(@logicflow/core@2.2.5)': + dependencies: + '@antv/hierarchy': 0.6.14 + '@logicflow/core': 2.2.5 + classnames: 2.5.1 + lodash-es: 4.18.1 + medium-editor: 5.23.3 + mobx: 5.15.7 + preact: 10.29.8 + rangy: 1.3.2 + vanilla-picker: 2.12.3 + transitivePeerDependencies: + - preact-render-to-string + '@lucide/vue@1.31.0(vue@3.5.41)': dependencies: vue: 3.5.41 @@ -2039,6 +2136,8 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@sphinxxxx/color-conversion@2.2.2': {} + '@standard-schema/spec@1.1.0': {} '@types/chai@5.2.3': @@ -2275,6 +2374,8 @@ snapshots: dependencies: readdirp: 5.1.1 + classnames@2.5.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2447,6 +2548,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hoist-non-react-statics@2.5.5: {} + hookable@5.5.3: {} html-encoding-sniffer@6.0.0: @@ -2598,6 +2701,8 @@ snapshots: pkg-types: 2.3.1 quansync: 0.2.11 + lodash-es@4.18.1: {} + lru-cache@10.4.3: {} lru-cache@11.5.2: {} @@ -2624,6 +2729,8 @@ snapshots: mdn-data@2.27.1: {} + medium-editor@5.23.3: {} + mime-db@1.52.0: {} mime-types@2.1.35: @@ -2643,6 +2750,20 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + mobx-preact@3.0.0(mobx@5.15.7)(preact@10.29.8): + dependencies: + hoist-non-react-statics: 2.5.5 + mobx: 5.15.7 + preact: 10.29.8 + + mobx-utils@5.6.2(mobx@5.15.7): + dependencies: + mobx: 5.15.7 + + mobx@5.15.7: {} + + mousetrap@1.6.5: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -2761,6 +2882,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.29.8: {} + proto-list@1.2.4: {} proxy-from-env@2.1.0: {} @@ -2769,6 +2892,8 @@ snapshots: quansync@0.2.11: {} + rangy@1.3.2: {} + readdirp@5.1.1: {} require-from-string@2.0.2: {} @@ -2902,6 +3027,12 @@ snapshots: rolldown: 1.2.4 vite: 8.2.1(sass@1.102.0)(yaml@2.9.0) + uuid@9.0.1: {} + + vanilla-picker@2.12.3: + dependencies: + '@sphinxxxx/color-conversion': 2.2.2 + vite@8.2.1(sass@1.102.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 diff --git a/code/fms/fms-vue/pnpm-workspace.yaml b/code/fms/fms-vue/pnpm-workspace.yaml index b4d7c864..d5619aec 100644 --- a/code/fms/fms-vue/pnpm-workspace.yaml +++ b/code/fms/fms-vue/pnpm-workspace.yaml @@ -1,4 +1,5 @@ allowBuilds: + '@logicflow/core': true '@parcel/watcher': true minimumReleaseAgeExclude: - '@vitest/coverage-v8@4.1.11' diff --git a/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue b/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue index ec3e0e93..3c232b61 100644 --- a/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue +++ b/code/fms/fms-vue/src/components/fms-module-edit/FmsModuleEditModal.vue @@ -492,7 +492,6 @@ function controlPropsOf(row) { v-for="section in sections" :key="section.key" :title="section.title" - :variant="section.title ? 'plain' : 'line'" :collapsible="false" :columns="24" > diff --git a/code/fms/fms-vue/src/components/fms-module-list/FmsQueryToolbar.vue b/code/fms/fms-vue/src/components/fms-module-list/FmsQueryToolbar.vue index 08de786b..bb0b19bd 100644 --- a/code/fms/fms-vue/src/components/fms-module-list/FmsQueryToolbar.vue +++ b/code/fms/fms-vue/src/components/fms-module-list/FmsQueryToolbar.vue @@ -50,9 +50,13 @@ const extraConditionCount = computed( extraQuickItems.value.filter((item) => hasConditionValue(filters[item.field.b_field])).length, ) +/* 条件列宽上限:4 列整行 = 4×288 + 3×16(列间距)= 1200px,不含右侧操作区。 + 窗口不够宽时列会等比收缩、只是到不了上限;改这里需同步 .query-grid 的兜底值 */ +const GRID_MAX_COLUMN_WIDTH = 288 + /** 网格列数按实际可见条件数收缩(最多 4 列):只有一个条件时不应占满 4 列宽 */ const gridStyle = computed(() => ({ - gridTemplateColumns: `repeat(${Math.min(Math.max(visibleQuickItems.value.length, 1), 4)}, minmax(0, 200px))`, + gridTemplateColumns: `repeat(${Math.min(Math.max(visibleQuickItems.value.length, 1), 4)}, minmax(0, ${GRID_MAX_COLUMN_WIDTH}px))`, })) function initializeDrafts() { @@ -85,9 +89,11 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true })
- -
+ + props.queryItems, initializeDrafts, { immediate: true }) align-items: flex-end; gap: 16px; } -/* 常用条件网格:一行最多 4 个、每列最大 200px;列数按实际条件数收缩(行内 style 控制),窄屏可收缩 */ +/* 常用条件网格:一行最多 4 个、每列最大 288px(整行 1200px,列数与上限由行内 style 下发, + 此处只是兜底);窗口不够宽时网格整体收缩、各列等比变窄,不横向溢出 */ .query-grid { display: grid; flex: 0 1 auto; min-width: 0; - grid-template-columns: repeat(4, minmax(0, 200px)); + grid-template-columns: repeat(4, minmax(0, 288px)); gap: 8px 16px; align-items: end; } @@ -182,10 +189,10 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true }) white-space: nowrap; text-overflow: ellipsis; } -/* 横向:宽度由内容决定(Form 传 label-width="auto"),只兜上限让超长字段名截断、 - 不把控件挤没;标签与控件的间距从组件库默认 12px 收到 6px */ +/* 横向:宽度来自 Form 的 label-width="72px"(定宽,短标签也占满); + 标签右对齐由 label-align="right" 在 Form 上传入,这里只把标签与控件的间距 + 从组件库默认 12px 收到 6px,给控件留出更多宽度 */ .query-fields.fms-form-horizontal .query-field :deep(.fms-form-item-label) { - max-width: 72px; padding-right: 6px; } /* 操作区:紧跟字段右侧、排成一行,右对齐;分「执行」「查询方式/配置」两组,中间竖分隔线 */ diff --git a/code/fms/fms-vue/src/components/ui/form/form-group.vue b/code/fms/fms-vue/src/components/ui/form/form-group.vue index a7937507..631eaa76 100644 --- a/code/fms/fms-vue/src/components/ui/form/form-group.vue +++ b/code/fms/fms-vue/src/components/ui/form/form-group.vue @@ -7,7 +7,8 @@ * 通过重新 provide columns 覆盖上下文,使组内 FormItem 的 span 自动按组内宫格计算。 * - FormItem 仍通过 provide/inject 拿到 Form 的 model/rules 校验上下文,组件不拦截。 * - 可选折叠功能(collapsible),默认展开,折叠/展开状态由内部管理或父级 v-model 控制。 - * - variant 控制标题视觉风格:line(左竖线 + 右折叠图标,默认)| plain(纯文字标题)。 + * - 视觉:整组为一张卡片,标题即卡头(淡底 + 下边框),折叠图标在标题文字左侧 + * (收起朝右 / 展开朝下)。与表单设计器的分组卡片语言一致。 */ import { computed, inject, provide, ref } from 'vue' import { ChevronDown } from '@lucide/vue' @@ -16,12 +17,6 @@ import { normalizeGap, fluidColumnGap } from './form-utils' const props = defineProps({ /** 分组标题;为空则不渲染标题行 */ title: { type: String, default: '' }, - /** 标题视觉风格:line(左竖线+右折叠图标)| plain(纯文字) */ - variant: { - type: String, - default: 'line', - validator: (value) => ['line', 'plain'].includes(value), - }, /** 组内宫格列数;默认继承外层 Form 的 columns(=1 时组内不建栅格,子项直接堆叠) */ columns: { type: Number, default: null }, /** 在外层 Form 宫格中的占格数;默认 1/-1 占满整行,可设 24 等实现并排 */ @@ -79,7 +74,7 @@ function toggleCollapse() { - - - -
- 演示控件 - 动作呈现 - - - 动作数量 - - - - 点模块左侧方框 = 该模块动作全给/全撤 | 点动作 = 单项开关 | 关掉页头「可访问」= 收回本页全部模块授权(灰色锁定) - -
- -
-
-

分配权限 · 张三(001)

- -
- -
- - -
- - -
-
-

海运工作台

- - -
- - -
-
- -
-
- 模块 - 动作 - 已选 -
-
-
-
-
-
- -
- - -
-
- - - - diff --git a/code/test/vue-vben-admin b/code/test/vue-vben-admin index 2d708760..edbc42ca 160000 --- a/code/test/vue-vben-admin +++ b/code/test/vue-vben-admin @@ -1 +1 @@ -Subproject commit 2d7087602b732e6c23c7696ee8430eb1e8221b56 +Subproject commit edbc42caaaa5b33fe61349063cc94cd36a1c5f30