审批状态回写
审批状态回写
业务用途
审批状态回写是连接 BPM 工作流与业务系统的关键纽带。
平台原生的审批状态是从 bpm_instance 表实时按 businessKey 派生的(见 BizApprovalController.buildApprovalStatus),不会落库到业务表。但 ERP 单据表等业务表自身携带 approval_status 列,为了让单据表的审批状态与流程实例保持一致(便于导出与 ERP 状态机对齐),BpmBusinessStatusCallback 在流程启动/终态时自动回写业务表的 approval_status 列。
核心价值
没有状态回写,业务表不知道自己的审批状态。查询采购单的审批状态需要关联 bpm_instance 表。有了状态回写,业务表自带 approval_status 列,查询时直接读取即可,无需关联 BPM 表。这对 ERP 状态机、数据导出、报表统计至关重要。
涉及文件
后端
| 文件 | 说明 |
|---|---|
backend/src/main/java/com/lowcode/service/bpm/BpmBusinessStatusCallback.java | 回写核心组件 |
backend/src/main/java/com/lowcode/service/bpm/SimpleFlowEngine.java | 引擎在 startProcess 和终态时调用回调 |
backend/src/main/java/com/lowcode/entity/LcForm.java | 表单实体(formCode -> tableId) |
backend/src/main/java/com/lowcode/entity/LcTableMeta.java | 表元数据(tableId -> tableName) |
backend/src/main/java/com/lowcode/entity/LcColumnMeta.java | 列元数据(检查是否有 approval_status 列) |
backend/src/main/java/com/lowcode/mapper/LcFormMapper.java | 表单 Mapper |
backend/src/main/java/com/lowcode/mapper/LcTableMetaMapper.java | 表元数据 Mapper |
backend/src/main/java/com/lowcode/mapper/LcColumnMetaMapper.java | 列元数据 Mapper |
数据库表
回写目标表结构要求
业务表必须包含 approval_status 列才能被回写。例如 ERP 采购申请表:
CREATE TABLE erp_purchase_request (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
-- ... 业务字段 ...
approval_status VARCHAR(20) DEFAULT 'DRAFT', -- ★ 审批状态列
-- ... 其他字段 ...
);approval_status 值映射
| BPM 实例状态 | 回写的 approval_status | 触发时机 |
|---|---|---|
running | IN_PROGRESS | startProcess 成功后 |
completed | APPROVED | 最后一个任务完成 |
rejected | REJECTED | 审批驳回 |
recalled | WITHDRAWN | 发起人撤回 |
canceled | WITHDRAWN | 取消 |
revoked | WITHDRAWN | 撤销 |
元数据表关系
bpm_instance.businessType (formCode)
│
▼
┌───────────────┐ formCode ┌──────────────┐ tableId ┌──────────────┐
│ LcForm │────────────►│ LcTableMeta │───────────►│ 物理业务表 │
│ formCode │ │ tableName │ │ erp_xxx │
│ tableId │ └──────────────┘ │ approval_ │
└───────────────┘ │ │ status │
│ tableId └──────────────┘
▼
┌──────────────┐
│ LcColumnMeta │
│ columnName │
│ (检查是否有 │
│ approval_ │
│ status 列) │
└──────────────┘后端实现
BpmBusinessStatusCallback 完整源码
@Component
public class BpmBusinessStatusCallback {
@Autowired private LcFormMapper formMapper;
@Autowired private LcTableMetaMapper tableMetaMapper;
@Autowired private LcColumnMetaMapper columnMetaMapper;
@Autowired private JdbcTemplate jdbcTemplate;
// 缓存:formCode -> tableId
private final ConcurrentHashMap<String, Long> tableIdByFormCode = new ConcurrentHashMap<>();
// 缓存:tableId -> 是否含 approval_status 列
private final ConcurrentHashMap<Long, Boolean> hasApprovalStatusByTable = new ConcurrentHashMap<>();
// 缓存:tableId -> 物理表名
private final ConcurrentHashMap<Long, String> tableNameByTableId = new ConcurrentHashMap<>();
/** 流程启动:业务行 approval_status 置 IN_PROGRESS */
public void onStarted(BpmInstance instance) {
writeBack(instance, "IN_PROGRESS");
}
/** 流程终态:按实例状态映射回写 */
public void onTerminal(BpmInstance instance) {
String mapped = mapStatus(instance.getStatus());
if (mapped != null) {
writeBack(instance, mapped);
}
}
// 实例状态 -> 业务表 approval_status 映射
private String mapStatus(String instanceStatus) {
if (instanceStatus == null) return null;
switch (instanceStatus) {
case "completed": return "APPROVED";
case "rejected": return "REJECTED";
case "recalled":
case "canceled":
case "revoked": return "WITHDRAWN";
default: return null;
}
}
}回写核心逻辑
private void writeBack(BpmInstance instance, String approvalStatus) {
try {
String businessKey = instance.getBusinessKey(); // "formCode:dataId"
String businessType = instance.getBusinessType(); // "formCode"
if (businessKey == null || businessType == null) return;
// 1. 从 businessKey 解析 dataId(最后一个冒号之后的部分)
int idx = businessKey.lastIndexOf(':');
if (idx <= 0) return;
Long dataId;
try {
dataId = Long.parseLong(businessKey.substring(idx + 1));
} catch (NumberFormatException e) {
return; // businessKey 格式不对,静默跳过
}
// 2. 通过 businessType(formCode) 查找 LcForm -> tableId
Long tableId = resolveTableId(businessType);
if (tableId == null) return;
// 3. 检查该表是否有 approval_status 列(opt-in 机制)
if (!hasApprovalStatusColumn(tableId)) return;
// 4. 获取物理表名
String tableName = resolveTableName(tableId);
if (tableName == null) return;
// 5. 执行 UPDATE
jdbcTemplate.update(
"UPDATE `" + tableName + "` SET approval_status = ? WHERE id = ?",
approvalStatus, dataId
);
} catch (Exception e) {
// 异常吞掉,不影响审批主流程
log.warn("BPM 业务状态回写失败 businessKey={} target={}: {}",
instance.getBusinessKey(), approvalStatus, e.getMessage());
}
}缓存机制
三个 ConcurrentHashMap 缓存避免每次回写都查数据库:
// formCode -> tableId(通过 LcForm 表查询)
private Long resolveTableId(String formCode) {
return tableIdByFormCode.computeIfAbsent(formCode, code -> {
LcForm form = formMapper.selectOne(new LambdaQueryWrapper<LcForm>()
.eq(LcForm::getFormCode, code)
.last("LIMIT 1"));
return form != null && form.getTableId() != null ? form.getTableId() : null;
});
}
// tableId -> 是否含 approval_status 列(通过 LcColumnMeta 表查询)
private boolean hasApprovalStatusColumn(Long tableId) {
return hasApprovalStatusByTable.computeIfAbsent(tableId, id -> {
Long count = columnMetaMapper.selectCount(new LambdaQueryWrapper<LcColumnMeta>()
.eq(LcColumnMeta::getTableId, id)
.eq(LcColumnMeta::getColumnName, "approval_status"));
return count != null && count > 0;
});
}
// tableId -> 物理表名(通过 LcTableMeta 表查询)
private String resolveTableName(Long tableId) {
return tableNameByTableId.computeIfAbsent(tableId, id -> {
LcTableMeta t = tableMetaMapper.selectById(id);
return t != null ? t.getTableName() : null;
});
}调用时机
SimpleFlowEngine 在以下位置调用回调:
// 1. startProcess 成功后(SimpleFlowEngine.java 第 99 行)
public BpmInstance startProcess(BpmProcessDefinitionInfo model, Map<String, Object> variables) {
// ... 创建实例、任务 ...
businessStatusCallback.onStarted(instance); // ★ 回写 IN_PROGRESS
return instance;
}
// 2. 审批驳回时(SimpleFlowEngine.java 第 416 行)
public void completeTask(BpmTask task, String action, String opinion) {
if ("reject".equals(action)) {
instance.setStatus("rejected");
businessStatusCallback.onTerminal(instance); // ★ 回写 REJECTED
}
}
// 3. 最后一个任务完成时(SimpleFlowEngine.java 第 548 行)
private void checkAndCompleteInstance(Long instanceId) {
if (pendingCount == 0) {
instance.setStatus("completed");
businessStatusCallback.onTerminal(instance); // ★ 回写 APPROVED
}
}
// 4. 撤回时(SimpleFlowEngine.java 第 915 行)
public void recallProcess(Long instanceId, Long userId) {
instance.setStatus("recalled");
businessStatusCallback.onTerminal(instance); // ★ 回写 WITHDRAWN
}
// 5. 会签驳回时(SimpleFlowEngine.java 第 527 行)
private void completeSignTask(BpmTask task, String action) {
if (rejectedCount > 0 && "AND".equals(signType)) {
instance.setStatus("rejected");
businessStatusCallback.onTerminal(instance); // ★ 回写 REJECTED
}
}回写全链路图
用户发起采购审批
│
▼
BizApprovalController.submit/{formId}
│
├─ schemaService.insertData(tableId, data) → INSERT INTO erp_purchase_request(...)
│ approval_status = 'DRAFT'(默认值)
│
├─ bpmModelService.startProcess(modelId, variables)
│ │
│ ▼
│ SimpleFlowEngine.startProcess()
│ │
│ ├─ INSERT bpm_instance (status='running')
│ ├─ INSERT bpm_task (status='pending')
│ │
│ └─ businessStatusCallback.onStarted(instance)
│ │
│ ├─ businessType = "purchase_request" (formCode)
│ ├─ businessKey = "purchase_request:100"
│ │
│ ├─ LcForm.formCode="purchase_request" → tableId=5
│ ├─ LcColumnMeta: tableId=5 有 approval_status 列? → YES
│ ├─ LcTableMeta: tableId=5 → tableName="erp_purchase_request"
│ │
│ └─ UPDATE erp_purchase_request
│ SET approval_status = 'IN_PROGRESS'
│ WHERE id = 100
│
▼
审批人通过
│
▼
SimpleFlowEngine.completeTask(task, "approve", opinion)
│
├─ UPDATE bpm_task SET status='completed'
│
└─ checkAndCompleteInstance()
│
├─ 无 pending 任务 → UPDATE bpm_instance SET status='completed'
│
└─ businessStatusCallback.onTerminal(instance)
│
├─ mapStatus("completed") → "APPROVED"
│
└─ UPDATE erp_purchase_request
SET approval_status = 'APPROVED'
WHERE id = 100安全策略
三重保护机制
- opt-in 机制:仅当业务表元数据(
LcColumnMeta)中存在approval_status列时才回写。基金会官网等不带该列的应用完全无副作用。 - 异常吞掉:任何异常都被
catch并记日志(log.warn),绝不影响审批主流程。即使回写失败,流程仍正常进行。 - businessKey 格式校验:
businessKey必须包含冒号且冒号后为数字 ID,否则静默跳过。
适合回写的场景
- ERP 采购单(
erp_purchase_request)、销售单等需要独立状态机的业务表 - 需要导出带审批状态的数据
- 需要在业务列表页直接展示审批状态(无需关联 BPM 表)
不适合回写的场景:
- 基金会官网等不带
approval_status列的表(自动跳过) - 不需要审批状态的简单表单
与 ERP 业务的联动示例
以 ERP 采购链为例:
-- 1. 发起采购审批前,业务表数据已存在
SELECT id, supplier_name, total_amount, approval_status FROM erp_purchase_request WHERE id = 100;
-- 结果: id=100, supplier_name='供应商A', total_amount=5000, approval_status='DRAFT'
-- 2. 发起审批后(onStarted 回写)
-- approval_status 变为 'IN_PROGRESS'
SELECT approval_status FROM erp_purchase_request WHERE id = 100;
-- 结果: approval_status='IN_PROGRESS'
-- 3. 审批通过后(onTerminal 回写)
-- approval_status 变为 'APPROVED'
SELECT approval_status FROM erp_purchase_request WHERE id = 100;
-- 结果: approval_status='APPROVED'
-- 4. 审批驳回后(onTerminal 回写)
-- approval_status 变为 'REJECTED'
SELECT approval_status FROM erp_purchase_request WHERE id = 100;
-- 结果: approval_status='REJECTED'
操作步骤
1. 确保业务表有 approval_status 列
在表设计器中为业务表添加 approval_status 字段(VARCHAR(20),默认值 DRAFT),或在 SQL 中手动添加:
ALTER TABLE erp_purchase_request ADD COLUMN approval_status VARCHAR(20) DEFAULT 'DRAFT';2. 确保 LcColumnMeta 有记录
表设计器添加字段后,lc_column_meta 表会自动生成记录。如果是手动 SQL 添加的列,需要手动插入元数据:
INSERT INTO lc_column_meta (table_id, column_name, column_type, column_comment)
VALUES (5, 'approval_status', 'varchar(20)', '审批状态');3. 绑定表单与流程模型
在表单设计器中:
- 设置
formCode为业务表名(如purchase_request) - 绑定
tableId指向业务表元数据 - 启用
approvalEnabled = true - 绑定
processModelId指向已部署的流程模型
4. 通过 BizApprovalController 发起审批
curl -X POST http://localhost:52856/api/biz-approval/submit/1 \
-H "Content-Type: application/json" \
-d '{"supplier_name": "供应商A", "total_amount": 5000}'BizApprovalController 会自动设置 businessKey = formCode:dataId,引擎启动后回调自动回写 approval_status。
常见问题
回写不生效?
检查以下几点:
- 业务表是否有
approval_status列(SELECT * FROM lc_column_meta WHERE table_id=X AND column_name='approval_status') bpm_instance.business_key是否为formCode:dataId格式bpm_instance.business_type是否为formCodeLcForm.form_code是否与business_type一致LcForm.table_id是否指向正确的LcTableMeta
缓存不会自动失效
BpmBusinessStatusCallback 使用 ConcurrentHashMap 缓存 formCode -> tableId、tableId -> hasApprovalStatus、tableId -> tableName 映射。如果运行时新增了 approval_status 列,需要重启应用让缓存失效。这在开发环境需要注意。
回写异常不会阻断流程
即使回写失败(如表名错误、列不存在、数据库异常),writeBack() 方法会 catch 异常并 log.warn,流程仍正常进行。这是设计上的「最终一致」策略:审批流程优先,状态回写尽力而为。
为什么不用数据库外键/触发器?
- 业务表是动态创建的(表设计器运行时建表),无法预写触发器。
- BPM 与业务表解耦,回写是「尽力而为」而非强约束。
- 异常吞掉策略确保审批流程不被业务表问题阻断。
