OA 印章管理系统实战(六):审批闭环——BusinessKey 为什么是流程解耦的核心?

好享购物的旧 OA 把申请单的所有字段塞进 Camunda 流程变量——variables.put("sealApplyJson", "{}")。跑了一个月,ACT_RU_VARIABLE 表炸成大 JSON blob,Camunda 自己都说这是反模式。这一章讲的是业务数据和流程数据分离怎么落地。

事故现场:把所有东西塞进流程变量
好享购物旧 OA 的 submit 逻辑(伪代码):
// 旧写法(反模式)
OaSealApply apply = mapper.selectById(id);
Map<String, Object> variables = new HashMap<>();
variables.put("sealApplyJson", JSON.toJSONString(apply)); // 整个申请单塞进一个变量!
variables.put("sealId", apply.getSealId()); // 单独字段也塞
variables.put("applicant", apply.getApplicant());
variables.put("purpose", apply.getPurpose());
variables.put("useCount", apply.getUseCount());
variables.put("fileName", apply.getFileName());
// ... 15 个字段全塞
runtimeService.startProcessInstanceByKey("seal_approve", variables);
一个月后 ACT_HI_VARINST(历史变量表)里,每个流程实例对应一条 JSON blob,平均 2.3KB。Camunda 查询历史流程时要反序列化这些大 JSON blob,一次审批列表查询从 200ms 变慢到 1.2s。更坑的是:申请单字段改了,流程里的 JSON blob 不会自动更新——两个数据源不同步。
排查:BusinessKey 到底是什么?
Camunda 官方对 BusinessKey 的定义很简短:A unique identifier for a process instance in the context of the calling application. 翻译过来就是——BusinessKey 是业务系统里这个流程实例的唯一标识,用来把 Camunda 的流程实例和业务系统的业务对象关联起来。
正确做法:
// 新写法(最佳实践,已在 ruoyi-oa 落地)
@Override
public String submitToWorkflow(Long applyId, Long modelId) {
OaSealApply apply = oaSealApplyMapper.selectOaSealApplyById(applyId);
// 流程变量只传少量关键字段,不传整个申请单!
Map<String, Object> variables = new HashMap<>();
variables.put("applicant", apply.getApplicant());
variables.put("sealId", apply.getSealId());
variables.put("purpose", apply.getPurpose());
variables.put("applyNo", apply.getApplyNo());
// BusinessKey = 申请单 ID
params.setBusinessKey(String.valueOf(apply.getId()));
params.setVariables(variables);
String processInstanceId = flowInstanceService.start(params);
// 回写 processInstanceId 和审批状态
OaSealApply update = new OaSealApply();
update.setId(apply.getId());
update.setProcessInstanceId(processInstanceId); // 业务表存流程实例 ID
update.setApprovalStatus("1"); // 审批中
oaSealApplyMapper.updateOaSealApply(update);
return processInstanceId;
}
四个步骤:
- 申请单落库(insertOaSealApply,approval_status=0 草稿)
- 以申请单 ID 为 BusinessKey 启动 Camunda 流程
- 回写
process_instance_id+approval_status=1(审批中) - 审批通过后回写
approval_status=2,驳回回写3
关键点:业务数据存业务表(oa_seal_apply),流程数据存 Camunda 表(ACT_RU_ / ACT_HI_*),两者通过 process_instance_id 和 BusinessKey 双向关联。*
底层原理:为什么流程变量不能存大字段?
Camunda 的流程变量持久化机制:
| 表 | 存储时机 | 内容 | |---|---|---| | ACT_RU_VARIABLE | 运行时 | 当前活跃的变量(含大 JSON blob) | | ACT_HI_VARINST | 流程结束后 | 历史变量快照(含大 JSON blob) | | ACT_HI_DETAIL | 变量变更时 | 每次变更的完整快照 |
如果流程变量里存了 sealApplyJson(2KB),一个 10 步审批流会产生:
- ACT_HI_VARINST:2KB
- ACT_HI_DETAIL:10 × 2KB = 20KB(每次任务完成都记录变量快照)
- 历史查询要反序列化这 22KB
如果改成 BusinessKey 关联:
- ACT_RU_VARIABLE / ACT_HI_VARINST:只存 applicant/sealId/purpose/applyNo 四个小字段(总共 < 1KB)
- 需要完整申请单数据时,用 BusinessKey 查业务表:
SELECT * FROM oa_seal_apply WHERE id = ?
查询对比:
-- 反模式:从流程变量里取业务数据
SELECT vi.TEXT_ AS seal_apply_json FROM ACT_HI_VARINST vi
WHERE vi.PROC_INST_ID_ = ? AND vi.NAME_ = 'sealApplyJson';
-- 得到一个 2.3KB 的 JSON blob,应用层要反序列化
-- 正确:用 BusinessKey 查业务表
SELECT * FROM oa_seal_apply WHERE id = ?;
-- 直接一行,不用反序列化
正确姿势:审批全链路调用
前端 → 后端 API → Camunda:
POST /oa/sealApply/submit
{ "applyId": 42, "modelId": 1 }
↓
OaSealApplyServiceImpl.submitToWorkflow(42, 1)
↓
IFlowInstanceService.start({
modelId: 1,
businessKey: "42", // 关键!
variables: { applicant: "zhangsan", sealId: 7, purpose: "签合同", applyNo: "SA20261008..." }
})
↓
Camunda: ProcessEngine.startProcessInstanceById(modelId, "42", variables)
↓
回写 oa_seal_apply SET process_instance_id=? approval_status='1'
↓
返回 processInstanceId
审批通过:
POST /oa/sealApply/approve
{ "applyId": 42 }
↓
OaSealApplyServiceImpl.approve(42)
↓
UPDATE oa_seal_apply SET approval_status='2' WHERE id=42
完整 curl 验证命令:
# 1. 登录拿 JWT
curl -X POST https://api.mashangan.com/api/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# 2. 查询待办任务
curl -H "Authorization: Bearer $TOKEN" \
https://api.mashangan.com/api/admin/workflow/task/myTodo
# 3. 审批通过(taskId 从待办列表拿)
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://api.mashangan.com/api/admin/workflow/task/complete \
-d '{"taskId":"xxx"}'
# 4. 验证申请单状态已回写
curl -H "Authorization: Bearer $TOKEN" \
https://api.mashangan.com/api/oa/sealApply/42
数据说话:两种模式的历史查询性能
| 场景 | 反模式(JSON blob 塞变量) | 正确模式(BusinessKey 关联) | |---|---|---| | 流程变量表大小(1000 个流程实例) | 2.3MB | 480KB | | 历史变量详情表大小 | 23MB | 4.8MB | | "查流程实例 42 的完整业务数据" | 2.3KB JSON blob → 反序列化 | 一行 SELECT,0.3ms | | "查所有申请单" | 反序列化 1000 个 blob | 分页查询业务表 |
变量大小从 2.3MB 降到 480KB,缩了 4.8 倍。 历史详情表缩了 4.8 倍。
面试怎么答:"Camunda 里 BusinessKey 怎么用?"
"BusinessKey 是业务系统用来把 Camunda 流程实例和业务对象关联起来的唯一标识。核心是业务数据和流程数据分离——seal_apply 表存申请单所有字段,ACT_RU_* 表存流程状态和少量关键字段,两者通过 process_instance_id(业务表存流程 ID)和 BusinessKey(流程存业务 ID)双向关联。
踩过的反模式:把整个申请单 JSON 塞进流程变量,一个月后 ACT_HI_VARINST 炸成大 blob,历史查询从 200ms 变慢到 1.2s。改进后流程变量只传 applicant、sealId、purpose、applyNo 四个小字段,需要完整业务数据时用 BusinessKey 查业务表。变量表缩了 4.8 倍。
还踩过一个坑:旧 OA 没有在审批通过/驳回后回写业务表状态——业务表 approval_status 永远是 1(审批中),导致业务表和流程表不同步。后来加了 approve()/reject() 方法专门回写 approval_status=2/3。
Camunda 8 的 Process Instance Key 就是 BusinessKey 的云原生版——我在第 03 章提过,如果将来升级到 SpringBoot 3 + Camunda 8,这个模式直接沿用就行。"
落地清单
submitToWorkflow() 伪代码骨架:
public String submitToWorkflow(Long applyId, Long modelId) {
OaSealApply apply = mapper.selectById(applyId);
if (apply == null) throw new RuntimeException("申请单不存在: " + applyId);
StartInstanceParams params = new StartInstanceParams();
params.setModelId(modelId);
params.setBusinessKey(String.valueOf(applyId)); // BusinessKey = 申请单 ID
params.setTitle(apply.getApplyNo() + "-" + apply.getPurpose());
// 流程变量只传少量关键字段
Map<String, Object> variables = new HashMap<>();
variables.put("applicant", apply.getApplicant());
variables.put("sealId", apply.getSealId());
variables.put("purpose", apply.getPurpose());
variables.put("applyNo", apply.getApplyNo());
params.setVariables(variables);
String processInstanceId = flowInstanceService.start(params);
// 回写流程实例 ID + 审批状态
OaSealApply update = new OaSealApply();
update.setId(apply.getId());
update.setProcessInstanceId(processInstanceId);
update.setApprovalStatus("1"); // 审批中
mapper.updateOaSealApply(update);
return processInstanceId;
}
本章素材文件:
| 文件 | 内容 |
|---|---|
| ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaSealApplyServiceImpl.java | 完整 submitToWorkflow() / approve() / reject() |
| ruoyi-oa/src/main/resources/mapper/oa/OaSealApplyMapper.xml | LEFT JOIN oa_seal_info + sys_dept |
| seal_approve.bpmn | 当前只有串行审批(部门主管审批→结束) |
下一章第 07 章讲审计留痕——一条 @OaAudit 注解搞定不可篡改。
