3 动态配置元数据模型设计
审批流的动态配置能力完全建立在规范化的元数据模型之上。本章定义五张核心元数据表及其关联关系:流程定义表、节点表、条件规则表、表单绑定表、字段JSON Schema。这五张表共同构成完整的流程元数据库,支撑后续的动态加载、运行时解析和可视化编辑。
3.1 五张元数据表结构
每张表均包含主键、外键约束、业务字段和审计字段。关键字段使用 JSON 类型储存非结构化配置,以适应审批场景的灵活扩展。
| 表名 | 主键 | 核心字段(示例) | 关联关系 |
|---|---|---|---|
| flow_def 流程定义表 | flow_def_id (UUID) | name, version, status, deployment_time, meta_version | 父表,被 node、form_bind 引用 |
| flow_node 节点表 | node_id (UUID) | flow_def_id, node_type (start/approval/condition/end), label, config_json, sort_order | FK → flow_def.flow_def_id ON DELETE CASCADE |
| cond_rule 条件规则表 | rule_id (UUID) | node_id, expression (JSON), target_node_id, priority | FK → flow_node.node_id;target_node_id 指向 flow_node.node_id |
| form_bind 表单绑定表 | bind_id (UUID) | flow_def_id, form_id, form_version, field_mapping (JSON), active | FK → flow_def.flow_def_id |
| field_schema 字段JSON Schema表 | schema_id (UUID) | bind_id, field_path (JSON), schema_json (JSON), validators (JSON) | FK → form_bind.bind_id ON DELETE CASCADE |
设计要点:
· 使用 UUID 作为主键,避免自增 ID 在分布式部署和动态配置导入时冲突。
· config_json / expression / field_mapping / schema_json 均使用原生 JSON 类型存储(MySQL 8.0+ JSON 或 PostgreSQL JSONB),支持高效查询与校验。
· 外键使用 ON DELETE CASCADE,确保删除流程定义时级联清理节点、规则和绑定关系。
· 使用 UUID 作为主键,避免自增 ID 在分布式部署和动态配置导入时冲突。
· config_json / expression / field_mapping / schema_json 均使用原生 JSON 类型存储(MySQL 8.0+ JSON 或 PostgreSQL JSONB),支持高效查询与校验。
· 外键使用 ON DELETE CASCADE,确保删除流程定义时级联清理节点、规则和绑定关系。
3.2 元数据关系与ER图
下表为五张表之间的物理外键关系图。注意 cond_rule 同时引用两个节点表记录,构成分支跳转回路。
图解说明: 矩形代表表,菱形箭头指向从表的主键,实线箭头的尾端为外键所在表。图中省略了审计字段(created_at、updated_at)以保持清晰。
3.3 创建元数据表的DDL语句
以 MySQL 8.0+ 为例,演示包含外键约束和级联策略的完整建表脚本。实际项目中建议使用数据库迁移工具(如 Flyway / Liquibase)管理版本。
-- 流程定义表
CREATE TABLE flow_def (
flow_def_id CHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
version VARCHAR(16) NOT NULL DEFAULT '1.0',
status ENUM('draft','active','archived') NOT NULL DEFAULT 'draft',
meta_version VARCHAR(16) NOT NULL DEFAULT '1.0',
deployment_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 节点表
CREATE TABLE flow_node (
node_id CHAR(36) NOT NULL PRIMARY KEY,
flow_def_id CHAR(36) NOT NULL,
node_type ENUM('start','approval','condition','end') NOT NULL,
label VARCHAR(128) NOT NULL,
config_json JSON,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_node_flow_def FOREIGN KEY (flow_def_id)
REFERENCES flow_def(flow_def_id) ON DELETE CASCADE,
INDEX idx_node_flow_order (flow_def_id, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 条件规则表
CREATE TABLE cond_rule (
rule_id CHAR(36) NOT NULL PRIMARY KEY,
node_id CHAR(36) NOT NULL,
expression JSON NOT NULL,
target_node_id CHAR(36) NOT NULL,
priority INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_rule_node FOREIGN KEY (node_id)
REFERENCES flow_node(node_id) ON DELETE CASCADE,
CONSTRAINT fk_rule_target FOREIGN KEY (target_node_id)
REFERENCES flow_node(node_id) ON DELETE CASCADE,
INDEX idx_rule_node (node_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 表单绑定表
CREATE TABLE form_bind (
bind_id CHAR(36) NOT NULL PRIMARY KEY,
flow_def_id CHAR(36) NOT NULL,
form_id VARCHAR(64) NOT NULL,
form_version VARCHAR(16) NOT NULL DEFAULT '1.0',
field_mapping JSON,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_bind_flow_def FOREIGN KEY (flow_def_id)
REFERENCES flow_def(flow_def_id) ON DELETE CASCADE,
INDEX idx_bind_def (flow_def_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 字段JSON Schema 表
CREATE TABLE field_schema (
schema_id CHAR(36) NOT NULL PRIMARY KEY,
bind_id CHAR(36) NOT NULL,
field_path JSON NOT NULL COMMENT '字段路径,如 ["invoice", "amount"]',
schema_json JSON NOT NULL COMMENT 'JSON Schema 片段',
validators JSON COMMENT '自定义校验规则覆盖',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_schema_bind FOREIGN KEY (bind_id)
REFERENCES form_bind(bind_id) ON DELETE CASCADE,
INDEX idx_schema_bind (bind_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
⚠️ 常见错误:级联删除顺序
如果
如果
cond_rule 的 target_node_id 指向自身所属流程定义下的节点,删除 flow_def 时,MySQL 会先删除外表引用的行。但如果 cond_rule 定义了两个外键指向 flow_node,必须确保 flow_node 不会因为级联先被删完导致 cond_rule 的 target 外键失效。实际测试表明,INNODB 的级联顺序按外键创建时间依次处理,不会出现死锁。建议在删除流程定义前先手动清理节点和规则,或利用事务回滚。生产环境强烈推荐使用外键 ON DELETE RESTRICT 并配合业务层级联逻辑。
3.4 字段 JSON Schema 设计要点
field_schema.schema_json 遵循 JSON Schema 规范,每个字段可独立定义类型、格式、枚举值、最小/最大值等。配合 field_path(JSON 数组),可灵活描述嵌套表单的校验规则。
-- 示例:发票审批表单的字段 schema(插入数据)
INSERT INTO field_schema (schema_id, bind_id, field_path, schema_json, validators) VALUES
(
UUID(),
'已存在的bind_id',
'["invoice", "amount"]',
'{"type": "number", "minimum": 0, "maximum": 999999, "title": "发票金额"}',
'{"required": true, "customLogic": "amount > 0 && amount < 1e6"}'
);
性能优化建议:
· 为
· 避免在 JSON 字段上直接使用
· 如果元数据频繁更新,考虑使用 ORM 二级缓存(如 Redis)减少数据库查询压力,但必须保证缓存失效策略与业务级联删除一致。
· 为
schema_json 和 expression 等 JSON 字段建立 虚拟列 + 索引(MySQL 8.0 支持 GENERATED COLUMN AS (schema_json->>"$.type")),可加速按 JSON 内部属性过滤的查询。· 避免在 JSON 字段上直接使用
LIKE 或全文检索,优先使用 JSON_CONTAINS 和 JSON_EXTRACT。· 如果元数据频繁更新,考虑使用 ORM 二级缓存(如 Redis)减少数据库查询压力,但必须保证缓存失效策略与业务级联删除一致。
3.5 调试与验证
编写 DDL 后,可通过以下方式验证元数据模型是否正确:
- 数据库本身检查:
SHOW CREATE TABLE flow_def;确认外键定义无误。 - 插入测试数据:插入一条流程定义,再插入对应节点和规则,确认无外键冲突。
- 级联删除测试:
DELETE FROM flow_def WHERE flow_def_id = 'xxx';检查子表是否自动清理(注意 MySQL 默认不报错但可能违反外键顺序)。 - JSON 校验:在应用层使用
JSON.is_valid()或数据库函数验证schema_json的语法是否正确。
3.6 部署注意事项
使用容器化部署时,建议在应用启动时自动执行初始化 DDL(通过 schema.sql 或 ORM 的自动建表功能)。如果使用 Kubernetes Init Container 初始化数据库,请确保 JSON 列名的字符集和排序规则一致(UTF-8)。
# 在构建阶段将 DDL 文件打包到镜像中
# 启动命令示例
wait-for-it db:3306 -- mysql -h db -u root -p$MYSQL_ROOT_PASSWORD < /docker-entrypoint-initdb.d/schema.sql
3.7 常见错误排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| Can't create table flow_node (errno: 150 - Foreign key constraint is incorrectly formed) | 外键引用的主表 flow_def 不存在或引擎非 InnoDB |
确认主表已创建且使用 ENGINE=InnoDB;检查字段类型是否匹配(CHAR vs VARCHAR) |
更新 cond_rule 时报告 Cannot add or update a child row |
target_node_id 指向的节点不属于同一个 flow_def 或者尚未插入 |
先插入所有节点,再插入规则;应用层校验目标节点存在且属于同一流程 |
DELETE FROM flow_def 后子表仍有残留数据 |
外键 ON DELETE 不是 CASCADE,或存储引擎不支持(MyISAM) | 改表为 InnoDB,并确保外键已正确添加 |
| JSON 字段插入后查询乱码 | 连接字符集不是 utf8mb4 | 设置连接 SET NAMES utf8mb4;;表字段使用 CHARSET utf8mb4 |