Files
workspace/code/fms/.trae/documents/删除规则一期实现方案.md
T
2026-09-17 21:29:08 +08:00

164 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 删除规则一期(配置侧)实现方案
## Context
「删除规则引擎」的设计已定稿([FMS删除规则引擎设计.md](file:///d:/workspace/code/fms/FMS删除规则引擎设计.md)),但代码侧**一期尚未开工**:`s_rule` 表不存在、`s_relation` 没有 `b_on_delete`、模块管理里没有规则 tab、后端无 `/data/deleteobj`。
本次做**一期:配置侧的元数据落地**(前端 + SQL 脚本,不含后端引擎)。完成后 IT 就能把规则配起来,但**不改变现有删除行为**——没有引擎去读这些配置。二期再写检查器/执行器与 `saveobjt` 接入。
职责划分(这是本次的设计主线):
| | 性质 | 落哪 | 配置入口 |
| --- | --- | --- | --- |
| 结构性删除动作(连带删除/连带归档) | 无条件,跟"谁引用谁"绑定 | `s_relation.b_on_delete` | 模块关联面板 |
| 引用检查(有引用时拒绝删除) | 无条件(存在引用即拒) | `s_rule`,`scope_type='relation'` | 模块关联面板勾选 |
| 业务条件(审核中/已销账/核销金额>0…) | 带条件、长尾 | `s_rule`,`scope_type='module'` | 模块管理「删除规则」tab(勾条件 / 写 SQL) |
关系表只说结构与结构性动作,**条件一律不进关系表**。
## 与设计文档的偏离(3 处,均为落地适配)
1. **`s_rule` 用业务联合主键** `(b_scope_type, b_scope_id, b_code)`,砍掉 `b_id`(bigint 雪花) 与 `ux_s_rule_code`。理由:全仓元数据表都用业务键/复合键,[开发规范.md](file:///d:/workspace/code/fms/开发规范.md#L187) 第 6 条要求配置表用业务键、不用雪花 ID;且文档的 `unique(b_code)` 与"规则可复制到其他模块"互相矛盾,**scope 内唯一**才能复制。
2. **`b_scope_id` 用 `varchar(250)`**:模块是编码 `varchar(50)`,关系是 4 列各 50 的复合键(拼串最长约 203),文档里的 `bigint` 装不下(这一点文档 §6.1 与 §6.2 本身不自洽)。
3. **DSL 的 `expr` 直接复用现有查询 AST**:`{"kind":"dsl","expr":buildAdvancedAst({nodes,groups})}`,即 `{type:'group',children:[{type:'condition',field,operator,value,join}…]}`。前端已有 `astToSql` 能把同构 AST 翻成 SQL 片段,避免自造第二套语法(文档 §6.3 的 `and/in` 写法是同一信息的另一种表述)。
另外遵循文档定案:**规则类型不单独存列**,由 `b_predicate` 推断——空 → 引用规则;可解析为 JSON → 勾条件;否则 → SQL 文本。`b_hook` 一期固定 `'delete.pre'`。`b_archived/b_archive_datetime` 一期**不落列**(引擎启用时才校验)。
## SQL(2 个文件)
`sql/fms_core.sql`(全新安装,紧接 §9 关系段之后新增第 9.1 节,并在 s_relation 建表段补列):
```sql
alter table dbo.s_relation add b_on_delete varchar(20) not null default 'none' -- none/cascade/archive
```
```sql
create table dbo.s_rule (
b_scope_type varchar(20) not null, -- module / relation
b_scope_id varchar(250) not null, -- module=模块编码;relation=源模块|源字段|目标模块|目标字段
b_code varchar(50) not null, -- 规则编码(scope 内唯一,复制/导入导出的稳定标识)
b_name nvarchar(100) not null, -- 规则名(如"审核中不可删")
b_hook varchar(30) not null, -- delete.pre(save.pre 留位)
b_predicate nvarchar(max) null, -- 空=引用规则;JSON=勾条件;其余=SQL 文本
b_message nvarchar(500) null, -- 提示文案(写 SQL 必填)
b_canuse tinyint not null default 1,
b_xh int not null default 0,
b_created_by varchar(50) null, b_created_at datetime2 null, -- 前端不提交,二期由服务端维护
b_updated_by varchar(50) null, b_updated_at datetime2 null,
primary key (b_scope_type, b_scope_id, b_code)
);
create index ix_s_rule_scope on dbo.s_rule (b_scope_type, b_scope_id, b_hook, b_canuse, b_xh);
```
- `b_on_delete` 用 `not null default 'none'`:存量行 alter 时回填 none,新增行与存量行取值一致 → 打开即不脏。
- 新增增量脚本 `sql/fms_delete_rule.sql`(沿用 [fms_rename_othercompany_category.sql](file:///d:/workspace/code/fms/sql/fms_rename_othercompany_category.sql) 的风格:头部注释写背景与前置核查「确认无 s_rule、s_relation 无 b_on_delete」,正文 `alter table` + `create table/index`,GO 分批)。已有库执行走 `fms-api/tools/migration/RunSqlFile.java`(GO 分批、单事务、失败整体回滚),先 `--dry-run` 再实跑。
- 后端**零改动**:`loaddata/saveobjt` 无表白名单,复合主键用逗号拼 `key_field`,deletes 按行内列值 AND 匹配。
## 前端改动
### 新增 3 个文件
**`src/views/module/module-management/ruleUtils.js`**
`relationScopeId(row)`(四列 `|` 拼串,与主键同序)/ `parseRulePredicate(row)`(三态)/ `ruleSummary(row, itemByField, t)`(dsl 走 `describeAdvancedQuery`,sql 取单行化前 80 字)/ `nextRuleCode(rules)`(`rule_{最大序号+1}`,可改)/ `createRuleRow(moduleId, seq)` / `createRefRuleRow(scopeId)`(`b_code='referenced'`,一个关系最多一条)/ `buildRuleQueryItems(fields, querySchema)`。
**`ModuleRulePanel.vue`**(样板:[ModuleRelationPanel.vue](file:///d:/workspace/code/fms/fms-vue/src/views/module/module-management/ModuleRelationPanel.vue))
- props:`moduleId / fields / querySchema / relations / refRulesReverse / loading`;`defineModel` 行数组 `rules`;`defineExpose({ validate })`。
- 列:`b_name` / `_typeText`(条件|SQL)/ `_summaryText`(条件中文摘要或 SQL 片段)/ `b_canuse`(checkbox 行内编辑)/ `b_xh`(拖拽重排 `(i+1)*10`)/ `actions`(编辑、删除)。派生列在 `watch(rules, deep)` 里就地补(`_` 前缀不落库、不判脏),补完换数组引用触发表格刷新。
- 工具栏:新增规则、批量删除。行点击/编辑 → 打开编辑弹窗。
- 只读区「来自模块关联」:列出 `refRulesReverse`(关系上的引用规则),列 源模块/源字段/目标模块/目标字段/说明;操作「查看」→ 当前模块的规则 `emit('navigate')` 由父级切到关系 tab,其它模块走 `onModuleSelect`(带未保存守卫)。
- `validate()`:规则名非空;`b_code` 非空、合法标识符、scope 内不重复;dsl 行至少一个有效条件;sql 行 `b_message` 非空。
**`ModuleRuleEditModal.vue`**(受控弹窗,参考 [UnsavedChangesModal.vue](file:///d:/workspace/code/fms/fms-vue/src/components/UnsavedChangesModal.vue) + `ui/Modal`)
- props `open / row / moduleId / items / existingCodes`;emits `update:open / ok`。
- 表单:规则名、规则编码(默认自动生成,可改)、类型 radio(勾条件 / 写 SQL)、启用、提示文案(sql 必填)。
- 勾条件分支:显示条件摘要 + 「编辑条件」→ 以**嵌套抽屉**打开 `FmsAdvancedQueryPanel`(`v-model:open` + `:items` + `v-model` 条件状态;Modal 与 Drawer 的滚动锁是引用计数、z-index 同层后挂载,已确认安全),`@apply` 回写状态与摘要。
- ok 时组装:dsl → `{"kind":"dsl","expr":buildAdvancedAst(state)}`;sql → 文本。
### 复用契约:`items` 怎么造(这是接条件面板的关键)
照抄 [FmsModuleListPage.vue](file:///d:/workspace/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue#L315-L334) 的组装方式:先 `buildQueryRender(query_schema)`(`@/components/fms-module-common/schemaRender`)取到 `conditions`(含 `operator/component/options/defaultValue`),与当前模块 `fields` 合并,产出
```js
{ config: { b_field, b_operator, b_component, b_default_value }, field, optionConfig, options }
```
`b_component` 缺省按字段类型推导(select→select、date/datetime→date-picker、number/money→number、checkbox→checkbox、其余 input);select 字段若查询配置里没有 options,降级为 input(提示直接填值),避免空下拉死路。**首版先做到这一步**——字段权限裁剪(查询侧有 `loadFieldPower`)本次不接,规则面板直接列全部启用字段。
### `queryUtils.js` 增一个反向函数
`src/components/fms-module-common/queryUtils.js` 已有 `buildAdvancedAst`(界面 → AST)但**没有反向**,回显已保存规则需要:
```js
export function advancedQueryFromAst(ast, { idFactory } = {})
// → { nodes, groups, degraded }
```
`condition` → node(`join` 缺省 'and');`group` → 组;组内再出现 group(>1 层)时把内层 children 拍平进该组并置 `degraded: true`,面板据此提示一次"原条件含多层分组,已按单层展开,请确认后保存"。
### `ModuleRelationPanel.vue` 加两列
- `b_on_delete`「删除连带」:select,选项 不连带/连带删除/连带归档;`many_to_one` 行 `disabled`,且 `onCellChange` 把关系类型切成 many_to_one 时自动置回 `none` 并 warning(防脏数据绕过校验)。
- `_hasRefRule`「有引用时拒绝删除」:checkbox(`_` 前缀,不落 s_relation、不参与关系行的 diff)。勾选/取消 → 在 `refRules` 草稿里增/删一条规则行(`scope_type='relation'`、`scope_id=relationScopeId(row)`)。
- **主键列变化处理**:关系行的源字段/目标模块/目标字段一旦改动,`scope_id` 就变了 → 自动取消勾选并删除对应规则行,提示"关系主键已变化,请重新勾选引用保护"。避免规则挂在旧键上产生孤儿行。
- props 增加 `v-model:refRules`(父级持有 `refRules/refRules_org` 成对草稿,走正常 diff 与脏判定,不需要任何 `_` 前缀的脏判定特例)。
### `index.vue` 接入点(9 处)
1. import `ModuleRulePanel`;`configGroups` 的 capability 组追加 `{ key: 'rule', label: '删除规则', icon: CircleSlash }`(已确认 @lucide/vue 有该图标)。
2. 数据对:`rules / rules_org`(scope=module)、`refRules / refRules_org`(scope=relation,源侧)、`refRulesReverse`(只读)。
3. `clearModuleData` 清空上述 ref;面板 ref `rulePanelRef`。
4. 模板挂载(relation 面板之后):`<ModuleRulePanel ref="rulePanelRef" v-model="rules" v-model:ref-rules="refRules" … @navigate="onRuleNavigate" />`,`onRuleNavigate` 切 `activeSection` 或 `onModuleSelect`。
5. `loadModuleConfig` 加第 7 张表:`s_rule`,条件 `b_scope_type='module' AND b_scope_id = <模块编码>`,排序 `b_xh, b_code`;关系侧规则**必须第二次查询**(规则表没有模块列,deletes 也不支持 LIKE 前缀):关系加载完后用双向关系键集合查 `b_scope_type='relation' AND b_scope_id IN (…)`,按 `scope_id` 属于源侧/目标侧拆成 `refRules_org` 与 `refRulesReverse`(无关系时跳过这次查询)。
6. `buildSaveData` 在 s_relation 段之后加两段(都先物化 scope 两列):
```js
for (const row of rules.value) { row.b_scope_type = 'module'; if (row.b_scope_id !== moduleId) row.b_scope_id = moduleId }
const ruleReq = tableChange('s_rule',
diffRows(stripInternalKeys(rules.value), stripInternalKeys(rules_org.value),
{ keyFields: ['b_scope_type', 'b_scope_id', 'b_code'] }),
['b_scope_type', 'b_scope_id', 'b_code'])
if (hasChanges(ruleReq)) saveReqs.push(ruleReq)
for (const row of refRules.value) { row.b_scope_type = 'relation'; row.b_hook = 'delete.pre' }
const refRuleReq = tableChange('s_rule',
diffRows(stripInternalKeys(refRules.value), stripInternalKeys(refRules_org.value),
{ keyFields: ['b_scope_type', 'b_scope_id', 'b_code'] }),
['b_scope_type', 'b_scope_id', 'b_code'])
if (hasChanges(refRuleReq)) saveReqs.push(refRuleReq)
```
7. `beforeSave`:接 `rulePanelRef.value?.validate?.()`,失败则 `activeSection='rule'` + 首条文案。
8. `hasUnsavedChanges` 的 pairs 追加 `[rules, rules_org]` 与 `[refRules, refRules_org]`。
9. `deleteModule`:追加模块级清理 `{ table:'s_rule', key_field:'b_scope_type,b_scope_id', deletes:[{ b_scope_type:'module', b_scope_id:id2 }] }`;关系级需**先查一次** `s_relation`(双向)拿到关系键,再一条批量 delete;确认文案补「删除规则」。
- 顺带:在关联面板删掉某条关系并保存时,规则行由 `refRules` 的 diff 自然删除(勾选态随行消失),无需额外逻辑。
## 边界与风险
- **many_to_one 禁连带**:面板 disabled + 改类型自动清 + `validate()` 三层拦截。
- **归档列校验点**:一期前端查不了 `sys.columns`,选「连带归档」时给一次性 `Message.warning('连带归档要求目标表含 b_archived / b_archive_datetime 列,引擎启用时会校验')`,真正的校验留在二期服务层。
- **打开即干净**:`b_on_delete` 非空默认值 + 派生列一律 `_` 前缀 + `refRules` 走常规 pairs(无脏判定特例)。
- **删除规则 tab 只对 data 模块显示**(与自动编码/权限/模块关联一致)。
## 验证
手工(`pnpm dev` → `/module/module-management`,需 g3soft 账号):
1. `RunSqlFile --dry-run` 后实跑增量脚本,核对 `s_rule` 与 `s_relation.b_on_delete`。
2. 关联面板新增 one_to_many 关系选「连带删除」→ 保存 → 库里 `b_on_delete='cascade'`。
3. 改成 many_to_one → 下拉只剩「不连带」且原值被清。
4. 勾「有引用时拒绝删除」→ 保存 → `s_rule` 出现 `scope_type='relation'`、`scope_id` 为四段竖线串的一行;取消勾选再保存 → 该行删除。
5. 改该关系的目标模块 → 勾选自动取消并提示;保存后库里无旧键孤儿规则。
6. 「删除规则」tab 新增勾条件规则(两条件 and/or 混用)→ 保存 → `predicate` 为 `{"kind":"dsl",…}`;重开模块条件与摘要一致。
7. 新增 SQL 规则不填提示文案 → 被拦。
8. 打开模块立即保存 → 提示「没有需要保存的变更」(不脏)。
9. 删除该模块 → `s_rule`/`s_relation` 残留为 0(含关系级规则)。
测试(一源文件一 spec,见 [tests/README.md](file:///d:/workspace/code/fms/fms-vue/tests/README.md)):
- 改 `tests/unit/query-ast.spec.js`:`advancedQueryFromAst` 空 AST / 单层往返 / 二层拍平置 `degraded`。
- 改 `tests/unit/module-relation-panel.spec.js`:`b_on_delete` 加载不置脏、many_to_one disabled 与自动清、cascade+many_to_one 被 validate 拦、勾选增删 `refRules` 行、改主键自动取消勾选。
- 新增 `tests/unit/module-rule-panel.spec.js`:回填只补 `_` 键、dsl 无条件被拦、sql 缺文案被拦、编码重复被拦、只读区渲染与 navigate。
- 新增 `tests/unit/module-rule-utils.spec.js`:scopeId 往返、predicate 三态(含非法 JSON 降级为 sql)、`nextRuleCode` 递增、items 推导。
- 改 `tests/views/module-unsaved-after-save.spec.js`:加载 7 张表 + 二次规则查询、`buildSaveData` 产出两条 `s_rule` 请求、`deleteModule` 的模块级与关系级清理。
## 明确不做(一期之外)
后端检查器/执行器与连带展开、`POST /data/deleteobj`(预览/错误列表)、`saveobjt` deletes 分支接入、`b_archived` 归档列、规则复制到其他模块、系统管理全局规则列表页、「值 vs 另一字段」比较操作符、条件面板的字段权限裁剪、`rule.edit` 权限点(页面已由 `meta.adminOnly` 兜底)。