Skip to content

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, PersistPostInterceptor
ts
// 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(流程唯一编码 = 表名约定)。表不存在 = 配置错误,流程执行失败(快速失败)
persistModeARCHIVE / 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 去前缀
amount800f_amount 去前缀
process_instance_id123流程上下文(幂等键)
apply_user_iduser1发起人
apply_dept_idD01发起部门
create_time / create_user2026-08-04 10:00:00 / user1(operator)writer 系统字段(1.6.3 起用户列默认取 operator)
update_time / update_user同上writer 系统字段
is_deleted0writer 系统字段

规则:

  • 业务表只需建你关心的列——多余的 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 节点)INSERTf_ 全量 + 上下文 + 系统字段
任务节点推进UPDATEf_(按节点字段权限过滤)+ 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. 注意事项(踩坑清单)

  1. 表不存在会报错relTableName 解析出来后表不存在 = 配置错误,流程执行失败(快速失败,不静默丢数据)
  2. 幂等是双层防护:① 同链节点级内存标记(__persist_executed_{instanceId}_{节点ID},1.8.0 起按节点放行——任务推进与结束定稿是不同节点都要生效);② process_instance_id 先查后插/更(跨请求/重启兜底)
  3. sys_ 前缀表拒绝写入:框架保留表空间,防误写
  4. ARCHIVE 只同意落库:不同意/退回(submitType=2)不入库;SYNC 驳回也入库(发起已落库,结束定稿 REJECT 状态)
  5. 引擎 1.6.2 起结束节点统一走节点执行链:后置拦截器在流程结束时完整触发(Go/Python/Node 修复了任务完成路径内联 finish 不触发拦截器的问题);1.8.0 起任务创建不触发拦截器(对齐 Java CreateTaskHandler),任务完成的拦截器由引擎在完成任务节点时显式触发
  6. Java 版挂载postInterceptors 反射无参实例化,writer 必须已注册到 ServiceContext(按类型查找),否则拦截器静默跳过
  7. 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 · 业务数据通用入库

jeeflow · 轻量级多语言工作流引擎