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

第 6 / 12 章
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;
}

四个步骤:

  1. 申请单落库(insertOaSealApply,approval_status=0 草稿)
  2. 以申请单 ID 为 BusinessKey 启动 Camunda 流程
  3. 回写 process_instance_id + approval_status=1(审批中)
  4. 审批通过后回写 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 注解搞定不可篡改。