Appearance
08 · 业务数据入库(流程结束自动落表)
版本:1.6.2 场景:流程审批通过后,把申请表单(
f_字段)自动写入独立业务表一条记录, 不再需要自己写持久化拦截器。
1. 适用场景
| 场景 | 说明 |
|---|---|
| 审批通过落库 | 流程结束 + 同意(submitType=AGREE)时,表单数据写入业务表 |
| 业务数据独立存储 | 流程数据在 jeeflow 表,业务数据在自有业务表(推荐架构) |
| 无引擎场景 | DynamicTableWriter 组件本身引擎无关,任意「表名 + 字段 Map 安全写表」 |
不适用:不同意/退回也要落库(当前语义仅同意落库;不同意走业务表状态字段需自行扩展拦截器)。
2. 三步接入
① 引入组件
xml
<!-- Java(Maven Central)——版本号由集成方父 pom 的 <jeeflow.version> 属性统一管理(参考 SDK 集成) -->
<dependency>
<groupId>com.mldong.jeeflow</groupId>
<artifactId>jeeflow-persist</artifactId>
<version>${jeeflow.version}</version>
</dependency>go
// Go(同 module 子包,无需新依赖)
import "github.com/mldong/jeeflow-go/persist"python
# Python(随主包)
from jeeflow.persist import JdbcDynamicTableWriter, PersistPostInterceptorts
// Node(随主包)
import { SqliteDynamicTableWriter, PersistPostInterceptor } from '@mldong/jeeflow'② 注册写入器
java
// Java——注册到引擎上下文(拦截器模型级反射实例化后按类型自取)
JdbcDynamicTableWriter writer = new JdbcDynamicTableWriter(dataSource);
// 可选:自定义系统字段列名(默认 create_time 等),null 禁用
writer.setCreateTimeColumn("create_time");
ServiceContext.put("dynamicTableWriter", writer);go
// Go——与拦截器一起注入引擎
writer := persist.NewJdbcDynamicTableWriter(db)
ic := persist.NewPersistPostInterceptor(writer, repo.FindDefineByID)
eng.SetExtensions(&engine.Extensions{Interceptors: []engine.FlowInterceptor{ic}})python
# Python
conn = sqlite3.connect("biz.db") # 或 pymysql/psycopg2 连接
writer = JdbcDynamicTableWriter(conn)
ic = PersistPostInterceptor(writer=writer, loader=repo.find_define_by_id)
eng.set_extensions(EngineExtensions(interceptors=[ic]))ts
// Node
const writer = new SqliteDynamicTableWriter(db)
const ic = new PersistPostInterceptor(writer, async id => repo.findDefineById(id))
engine.setExtensions({ interceptors: [ic] })③ 流程定义声明落库
持久化行为由流程定义 JSON 顶层的字段驱动(与 name/nodes 同层)——四语言统一契约:
| 字段 | 取值 | 语义 |
|---|---|---|
relTableName | 表名 | 业务表名——数据写入哪张表;缺省回落流程 name(流程唯一编码 = 表名约定)。表不存在 = 配置错误,流程执行失败(快速失败) |
persistMode | ARCHIVE / SYNC(缺省 ARCHIVE) | 持久化模式——ARCHIVE:流程结束且同意时落库一次;SYNC:发起即入库 → 节点推进 → 结束定稿,全程留痕(详见 §2.1)。非 SYNC 值一律回落 ARCHIVE |
json
{
"name": "leave",
"displayName": "请假审批",
"type": "approval",
"relTableName": "biz_leave",
"persistMode": "ARCHIVE",
"nodes": []
}拦截器挂载(行为统一,机制按引擎架构):Java 在流程定义顶层加 "postInterceptors": "com.mldong.jeeflow.persist.interceptor.PersistPostInterceptor" (引擎节点执行后反射实例化,writer 注册 ServiceContext);Go/Python/Node 拦截器实例挂引擎全局 Extensions(见下方各语言示例)。两种方式等价——拦截器只对声明了 relTableName 的流程生效。
解析链:Java 经
LfModel → ModelParser → ProcessModel;Go/Python/Node 经DefineLoader从流程定义 content 解析同一批字段——四语言契约一致。
3. 落库字段
以请假流程为例,发起时提交表单变量:
f_title = "年假申请"
f_amount = 800
u_deptId = "D01"流程结束同意后写入 biz_leave 表:
| 列 | 值 | 来源 |
|---|---|---|
title | 年假申请 | f_title 去前缀 |
amount | 800 | f_amount 去前缀 |
process_instance_id | 123 | 流程上下文(幂等键) |
apply_user_id | user1 | 发起人 |
apply_dept_id | D01 | 发起部门 |
create_time / create_user | 2026-08-04 10:00:00 / user1(operator) | writer 系统字段(1.6.3 起用户列默认取 operator) |
update_time / update_user | 同上 | writer 系统字段 |
is_deleted | 0 | writer 系统字段 |
规则:
- 业务表只需建你关心的列——多余的
f_字段自动过滤(列探测) - 非自增主键表(1.6.5):雪花/应用生成主键(如 mldong
id雪花)需注册setPrimaryKeyGenerator(雪花IdWorker)——data 已有主键值用之;未配置生成器时抛清晰错误(表/主键列名+配置指引) - 列名宽松匹配(1.6.4):驼峰表单字段(
companyName)自动落到下划线表列(company_name),写入用表列原名;需要精确控制时setStrictColumnMatch(true) - 流程上下文字段与系统字段为蛇形列名约定:
process_instance_id/apply_user_id/apply_dept_id/create_time/create_user/update_time/update_user/is_deleted tf_任务字段(如审批意见)在 ARCHIVE 模式不入库;SYNC 模式下冗余到业务表对应列(见 §2.1)
2.1 同步演进模式(SYNC,1.8.0)
流程定义顶层加 "persistMode": "SYNC",改为全程留痕——提交申请即入库,任务节点推进更新,结束定稿最终状态,不管成功失败都入库:
json
{
"name": "leave",
"displayName": "请假审批",
"type": "approval",
"relTableName": "biz_leave",
"persistMode": "SYNC",
"postInterceptors": "com.mldong.jeeflow.persist.interceptor.PersistPostInterceptor",
"nodes": []
}(persistMode 缺省 ARCHIVE——保持 1.6.2 起的"结束同意归档"行为不变)
执行时序
| 时机 | 动作 | 表内数据 |
|---|---|---|
| 发起(start 节点) | INSERT | f_ 全量 + 上下文 + 系统字段 |
| 任务节点推进 | UPDATE | f_(按节点字段权限过滤)+ tf_ 冗余 + 状态列=10(DOING) |
| 结束(同意) | UPDATE | 状态列=20(FINISHED) |
| 结束(驳回) | UPDATE | 状态列=45(REJECT),数据保留 |
状态字段
状态列名约定:值 = 流程实例状态码(10 DOING / 20 FINISHED / 45 REJECT / 50 PENDING), 列名优先 {节点ID}_{状态码}(如 task1_10),无该列回落 {节点ID}(如 task1)。
sql
-- 业务表(SYNC 示例)
CREATE TABLE biz_leave (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100), -- f_title
amount DECIMAL(10,2), -- f_amount
opinion VARCHAR(200), -- tf_opinion(审批意见冗余,可选)
apply INT, -- 状态列:节点 apply(发起申请)
task1 INT, -- 状态列:节点 task1(上级审批)
finish INT, -- 状态列:结束节点(最终状态)
process_instance_id BIGINT,
apply_user_id VARCHAR(50),
...
);字段权限(任务节点级)
任务节点 properties.field 声明该节点哪些表单字段可更新(vben5-wf 机制,1.8.0 起)。 键格式双兼容(1.8.1 起):PERMISSION_f_{表单字段全名}(前端设计器约定,优先) 与 PERMISSION_{去前缀名}(1.8.0 首版格式,兼容)——两者都匹配:
json
{
"id": "task1",
"type": "snaker:task",
"properties": {
"assignee": "leader",
"field": { "PERMISSION_f_title": 1, "PERMISSION_amount": 2 }
}
}上面示例:
title只读(前端格式PERMISSION_f_title)、amount可编辑(兼容格式PERMISSION_amount)——两种键格式可混用。1.8.0 首版只认PERMISSION_{去前缀名}, 与前端约定不一致导致只读/隐藏失效,1.8.1 起修复。
| 值 | 语义 | 持久化 |
|---|---|---|
| 缺省 | 可编辑 | 更新 |
| 1 | 只读 | 不更新 |
| 2 | 可编辑 | 更新 |
| 3 | 隐藏 | 不更新 |
只有任务节点按权限过滤更新;结束/网关等非任务节点不覆盖业务字段(只定稿状态), 避免全量覆盖任务节点的只读/隐藏限制。
4. 注意事项(踩坑清单)
- 表不存在会报错:
relTableName解析出来后表不存在 = 配置错误,流程执行失败(快速失败,不静默丢数据) - 幂等是双层防护:① 同链节点级内存标记(
__persist_executed_{instanceId}_{节点ID},1.8.0 起按节点放行——任务推进与结束定稿是不同节点都要生效);②process_instance_id先查后插/更(跨请求/重启兜底) sys_前缀表拒绝写入:框架保留表空间,防误写- ARCHIVE 只同意落库:不同意/退回(submitType=2)不入库;SYNC 驳回也入库(发起已落库,结束定稿 REJECT 状态)
- 引擎 1.6.2 起结束节点统一走节点执行链:后置拦截器在流程结束时完整触发(Go/Python/Node 修复了任务完成路径内联 finish 不触发拦截器的问题);1.8.0 起任务创建不触发拦截器(对齐 Java CreateTaskHandler),任务完成的拦截器由引擎在完成任务节点时显式触发
- Java 版挂载:
postInterceptors反射无参实例化,writer 必须已注册到ServiceContext(按类型查找),否则拦截器静默跳过 - SYNC 状态列:状态字段按当前节点探测(
{节点ID}_{状态码}→{节点ID}),表无对应列则跳过不报错;任务节点统一写 DOING(任务推进状态),结束节点写最终状态
5. 验证
sql
-- ARCHIVE:跑完一条审批流(同意)后:
SELECT * FROM biz_leave;
-- 应恰好 1 条记录,且 process_instance_id = 流程实例 ID
-- 重复触发同一实例的结束(如重放请求):记录数仍为 1(幂等)
-- SYNC:发起后即有记录(状态列=apply 未动),推进后状态更新,结束后定稿:
SELECT title, apply, task1, finish FROM biz_leave WHERE process_instance_id = 123;
-- 同意 → finish=20;驳回 → finish=45(记录保留,不删除)四语言合规测试矩阵见 规范 09 · 业务数据通用入库。