164 lines
15 KiB
Markdown
164 lines
15 KiB
Markdown
# 删除规则一期(配置侧)实现方案
|
||
|
||
## 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` 兜底)。 |