会签/转办/委派/撤回/催办/加签
会签/转办/委派/撤回/催办/加签
业务用途
除了基本的审批通过/驳回,平台还支持以下高级审批操作:
- 会签(Sign):多人同时审批同一节点,按规则(全票/半数/比例)判定是否通过
- 转办(Transfer):将任务转交给他人处理,原审批人不再参与
- 委派(Delegate):将任务委托给他人处理,委托人处理后任务回归
- 撤回(Recall):发起人在任务被处理前撤回整个流程
- 催办(Urge):催促当前审批人尽快处理
- 加签(Add Sign):在会签节点临时增加审批人
- 关注(Follow):关注任务进展,任务完成时收到通知
涉及文件
后端
| 文件 | 说明 |
|---|---|
backend/src/main/java/com/lowcode/service/bpm/SimpleFlowEngine.java | completeSignTask / transferTaskRecord / delegateTaskRecord / recallProcess / addSignTask / followTask |
backend/src/main/java/com/lowcode/controller/bpm/BpmTaskSignController.java | 会签控制器(/api/bpm/sign) |
backend/src/main/java/com/lowcode/controller/bpm/BpmTaskController.java | 任务控制器(转办/委派/催办/撤回/关注) |
backend/src/main/java/com/lowcode/controller/bpm/BpmInstanceController.java | 实例控制器(撤回) |
backend/src/main/java/com/lowcode/service/impl/bpm/BpmTaskSignServiceImpl.java | 会签服务实现 |
backend/src/main/java/com/lowcode/entity/bpm/BpmTaskSign.java | 会签实体 |
backend/src/main/java/com/lowcode/entity/bpm/BpmTaskTransferRecord.java | 转办/委派记录实体 |
backend/src/main/java/com/lowcode/entity/bpm/BpmTaskFollow.java | 任务关注实体 |
前端
| 文件 | 说明 |
|---|---|
frontend/src/views/bpm/todo.vue | 待办页(含转办/委派/催办操作) |
frontend/src/views/bpm/instance.vue | 实例页(含撤回操作) |
数据库表
bpm_task_sign(会签子任务表)
CREATE TABLE bpm_task_sign (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_id BIGINT NOT NULL, -- 主任务 ID
instance_id BIGINT NOT NULL, -- 流程实例 ID
node_id VARCHAR(100), -- 节点标识
sign_type VARCHAR(20) DEFAULT 'AND', -- ★ AND-会签 OR-或签
sign_rule VARCHAR(20) DEFAULT 'ALL', -- ★ ALL-全票 HALF-半数 RATIO-比例
pass_ratio INT DEFAULT 100, -- 通过比例(0-100)
assignee_id BIGINT NOT NULL, -- 审批人 ID
assignee_name VARCHAR(100), -- 审批人姓名
sort_order INT DEFAULT 1, -- 排序号(依次审批时使用)
status VARCHAR(20) DEFAULT 'pending', -- pending/approved/rejected/skip
opinion TEXT, -- 审批意见
approve_time TIMESTAMP, -- 审批时间
is_add_sign INT DEFAULT 0, -- 是否加签
add_sign_type VARCHAR(20), -- BEFORE-前加签 AFTER-后加签
add_sign_user_id BIGINT, -- 加签人 ID
add_sign_user_name VARCHAR(100), -- 加签人姓名
parent_sign_id BIGINT, -- 父会签任务 ID
signature_url VARCHAR(500), -- 签名图片 URL
duration BIGINT -- 审批耗时(秒)
);bpm_task_transfer_record(转办/委派记录表)
CREATE TABLE bpm_task_transfer_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
instance_id BIGINT, -- 流程实例 ID
task_id BIGINT, -- 任务 ID
transfer_type VARCHAR(20), -- ★ TRANSFER-转办 DELEGATE-委派
from_user_id BIGINT, -- 原审批人 ID
from_user_name VARCHAR(50), -- 原审批人姓名
to_user_id BIGINT, -- 新审批人 ID
to_user_name VARCHAR(50), -- 新审批人姓名
reason VARCHAR(500), -- 原因
operator_id BIGINT, -- 操作人 ID
operator_name VARCHAR(50), -- 操作人姓名
transfer_time TIMESTAMP -- 转派时间
);bpm_task_follow(任务关注表)
CREATE TABLE bpm_task_follow (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_id BIGINT, -- 任务 ID
instance_id BIGINT, -- 实例 ID
user_id BIGINT, -- 关注人 ID
user_name VARCHAR(50), -- 关注人姓名
follow_time TIMESTAMP, -- 关注时间
notified BOOLEAN, -- 是否已通知
notify_time TIMESTAMP -- 通知时间
);后端实现
会签机制
会签任务创建
SimpleFlowEngine.createTask() 方法在遇到 sign 类型节点时,为每个审批人创建独立的子任务:
private void createTask(BpmInstance instance, Map<String, Object> node, Map<String, Object> variables) {
List<SysUser> assignees = resolveAssignees(node, variables);
String nodeType = String.valueOf(node.get("type"));
if ("sign".equals(nodeType) || assignees.size() > 1) {
// 创建主任务(记录会签配置)
BpmTask mainTask = buildBaseTask(instance, node);
mainTask.setIsSign(1);
mainTask.setSignType((String) node.getOrDefault("signType", "AND"));
mainTask.setSignRule((String) node.getOrDefault("signRule", "ALL"));
mainTask.setPassRatio(ratio);
mainTask.setTotalSignCount(assignees.size());
mainTask.setCandidateUserIds(joinUserIds(assignees));
taskMapper.insert(mainTask);
// 为每个审批人创建子任务
for (SysUser assignee : assignees) {
BpmTask signTask = buildBaseTask(instance, node);
signTask.setAssigneeId(assignee.getId());
signTask.setAssigneeName(getUserDisplayName(assignee));
signTask.setIsSign(1);
signTask.setSignType(mainTask.getSignType());
signTask.setSignRule(mainTask.getSignRule());
signTask.setPassRatio(mainTask.getPassRatio());
taskMapper.insert(signTask);
// 发送待办通知
notificationService.sendTodoNotification(...);
}
return;
}
// 单人审批...
}会签规则判定
completeSignTask() 方法在每次会签子任务完成时统计结果并判定:
private void completeSignTask(BpmTask task, String action) {
// 获取该会签节点的所有子任务
List<BpmTask> signTasks = taskMapper.selectList(...);
// 统计已通过/已拒绝数量
int approvedCount = 0;
int rejectedCount = 0;
for (BpmTask st : signTasks) {
if ("completed".equals(st.getStatus())) {
approvedCount++;
} else if ("rejected".equals(st.getStatus())) {
rejectedCount++;
}
}
String signType = task.getSignType(); // AND 或 OR
String signRule = task.getSignRule(); // ALL / HALF / RATIO
Integer passRatio = task.getPassRatio();
int totalCount = signTasks.size();
boolean isPass = false;
if ("AND".equals(signType)) {
// 会签:所有审批人参与,按规则判定
if ("ALL".equals(signRule)) {
isPass = (approvedCount == totalCount && rejectedCount == 0);
} else if ("HALF".equals(signRule)) {
isPass = (approvedCount > totalCount / 2);
} else if ("RATIO".equals(signRule)) {
double approvalRatio = (double) approvedCount / totalCount * 100;
isPass = (approvalRatio >= passRatio);
}
} else {
// 或签:任一审批人通过即可
isPass = (approvedCount >= 1);
}
if (isPass) {
task.setStatus("sign_passed");
} else if (rejectedCount > 0 && "AND".equals(signType)) {
task.setStatus("sign_rejected");
// 会签被拒绝,整个实例置为 rejected
instance.setStatus("rejected");
businessStatusCallback.onTerminal(instance);
}
}会签类型与规则对照
| signType | 含义 | signRule | 判定条件 |
|---|---|---|---|
| AND | 会签(所有人参与) | ALL | 全票通过(approvedCount == total && rejectedCount == 0) |
| AND | 会签 | HALF | 半数以上通过(approvedCount > total / 2) |
| AND | 会签 | RATIO | 按比例通过(approvedCount / total * 100 >= passRatio) |
| OR | 或签 | -- | 任一通过即可(approvedCount >= 1) |

转办(Transfer)
转办将任务交给他人处理,原任务标记为 transferred,创建新 pending 任务给目标人:
// SimpleFlowEngine.transferTaskRecord()
@Transactional
public void transferTaskRecord(Long taskId, Long fromUserId, String fromUserName,
Long toUserId, String toUserName, String reason, Long operatorId, String operatorName) {
BpmTask task = taskMapper.selectById(taskId);
if (!"pending".equals(task.getStatus())) {
throw new IllegalStateException("任务已处理,无法转移");
}
// 1. 记录转派历史
BpmTaskTransferRecord record = new BpmTaskTransferRecord();
record.setTransferType("TRANSFER");
record.setFromUserId(fromUserId);
record.setToUserId(toUserId);
record.setReason(reason);
transferRecordMapper.insert(record);
// 2. 原任务标记为 transferred
task.setStatus("transferred");
taskMapper.updateById(task);
// 3. 创建新任务给目标人
BpmTask newTask = new BpmTask();
newTask.setAssigneeId(toUserId);
newTask.setAssigneeName(toUserName);
newTask.setStatus("pending");
taskMapper.insert(newTask);
// 4. 通知
notificationService.sendTodoNotification(...);
notificationService.sendTransferNotification(...);
}委派(Delegate)
委派直接更换审批人,不创建新任务,记录委托历史:
// SimpleFlowEngine.delegateTaskRecord()
@Transactional
public void delegateTaskRecord(Long taskId, Long fromUserId, String fromUserName,
Long toUserId, String toUserName, String reason, Long operatorId, String operatorName) {
BpmTask task = taskMapper.selectById(taskId);
if (!"pending".equals(task.getStatus())) {
throw new IllegalStateException("任务已处理,无法委托");
}
// 1. 记录委托历史
BpmTaskTransferRecord record = new BpmTaskTransferRecord();
record.setTransferType("DELEGATE");
transferRecordMapper.insert(record);
// 2. 直接更换审批人(不创建新任务)
task.setAssigneeId(toUserId);
task.setAssigneeName(toUserName);
taskMapper.updateById(task);
// 3. 通知原审批人和新审批人
notificationService.sendDelegateNotification(...);
}转办 vs 委派的区别
- 转办:原任务标记为
transferred,创建新的pending任务给目标人。原审批人不再参与。 - 委派:不创建新任务,直接修改原任务的
assigneeId。委托人处理完后任务直接完成。

撤回(Recall)
撤回仅在流程未被任何审批人处理前可用:
// SimpleFlowEngine.recallProcess()
@Transactional
public void recallProcess(Long instanceId, Long userId) {
BpmInstance instance = instanceMapper.selectById(instanceId);
if (!"running".equals(instance.getStatus())) {
throw new IllegalStateException("只能撤回运行中的流程");
}
if (!userId.equals(instance.getStartUserId())) {
throw new IllegalStateException("只有流程发起人可以撤回");
}
// 检查是否有已完成的任务(有则不能撤回)
long completedCount = taskMapper.selectCount(...);
if (completedCount > 0) {
throw new IllegalStateException("流程已被处理,无法撤回");
}
// 删除所有待办任务
List<BpmTask> pendingTasks = taskMapper.selectList(...);
for (BpmTask t : pendingTasks) {
taskMapper.deleteById(t.getId());
}
// 更新实例状态
instance.setStatus("recalled");
instance.setEndTime(LocalDateTime.now());
instanceMapper.updateById(instance);
// 业务表回写:WITHDRAWN
businessStatusCallback.onTerminal(instance);
}加签(Add Sign)
在会签节点临时增加审批人:
// SimpleFlowEngine.addSignTask()
@Transactional
public void addSignTask(Long taskId, Long newAssigneeId, String newAssigneeName) {
BpmTask task = taskMapper.selectById(taskId);
if (task.getIsSign() == null || task.getIsSign() != 1) {
throw new IllegalArgumentException("非会签节点不支持加签");
}
// 创建新的会签子任务
BpmTask signTask = new BpmTask();
signTask.setAssigneeId(newAssigneeId);
signTask.setAssigneeName(newAssigneeName);
signTask.setStatus("pending");
signTask.setIsSign(1);
signTask.setSignType(task.getSignType());
signTask.setSignRule(task.getSignRule());
signTask.setPassRatio(task.getPassRatio());
taskMapper.insert(signTask);
// 更新主任务的总数
task.setTotalSignCount(task.getTotalSignCount() + 1);
taskMapper.updateById(task);
}催办(Urge)
发送催办通知给当前审批人:
// BpmTaskController.java
@PostMapping("/urge/{id}")
public Result<Void> urge(@PathVariable Long id,
@RequestParam(required = false) Long operatorId,
@RequestParam(required = false) String operatorName) {
BpmTask task = taskService.getById(id);
notificationService.sendUrgentNotification(
task.getId(), task.getTaskName(),
task.getAssigneeId(), task.getAssigneeName(),
operatorId, operatorName
);
return Result.success("催办成功,已发送提醒通知");
}关注(Follow)
关注任务后,任务完成时收到通知:
// SimpleFlowEngine.followTask()
public void followTask(Long taskId, Long userId, String userName) {
// 检查是否已关注
BpmTaskFollow existing = taskFollowMapper.selectOne(...);
if (existing != null) return; // 已关注,不重复
BpmTaskFollow follow = new BpmTaskFollow();
follow.setTaskId(taskId);
follow.setUserId(userId);
follow.setUserName(userName);
follow.setNotified(false);
taskFollowMapper.insert(follow);
}任务完成时,notifyTaskFollowers() 方法通知所有关注者:
private void notifyTaskFollowers(BpmTask task, String action) {
List<BpmTaskFollow> followers = taskFollowMapper.selectList(...);
for (BpmTaskFollow follow : followers) {
notificationService.sendTaskFollowNotification(
task.getId(), task.getTaskName(),
follow.getUserId(), follow.getUserName(), action
);
follow.setNotified(true);
follow.setNotifyTime(LocalDateTime.now());
taskFollowMapper.updateById(follow);
}
}端点一览
BpmTaskSignController(会签)
基础路径:/api/bpm/sign
| HTTP | 路径 | 参数 | 说明 |
|---|---|---|---|
| GET | /task/{taskId} | taskId | 获取任务的所有会签子任务 |
| GET | /instance/{instanceId} | instanceId | 获取实例的所有会签任务 |
| POST | /approve/{signId} | signId, userId, userName, opinion(可选) | 会签审批通过 |
| POST | /reject/{signId} | signId, userId, userName, opinion(可选) | 会签审批驳回 |
| POST | /addBefore/{signId} | signId, targetUserId, targetUserName, operatorId, operatorName, reason(可选) | 前加签 |
| POST | /addAfter/{signId} | signId, targetUserId, targetUserName, operatorId, operatorName, reason(可选) | 后加签 |
| POST | /create | taskId, instanceId, nodeId, signType, signRule, passRatio, Body: List<String> assignees | 批量创建会签任务 |
| GET | /{signId} | signId | 获取会签任务详情 |
BpmTaskController(转办/委派/催办/撤回/关注)
基础路径:/api/bpm/task
| HTTP | 路径 | 参数 | 说明 |
|---|---|---|---|
| POST | /transfer/{id} | id, targetUserId, targetUserName | 转办(简单版,直接换人) |
| POST | /transferTask/{id} | id, fromUserId, fromUserName, toUserId, toUserName, reason(可选), operatorId(可选), operatorName(可选) | 转办(记录历史) |
| POST | /delegate/{id} | id, delegateUserId, delegateUserName | 委托(简单版) |
| POST | /delegateTask/{id} | id, fromUserId, fromUserName, toUserId, toUserName, reason(可选), operatorId(可选), operatorName(可选) | 委派(记录历史) |
| POST | /urge/{id} | id, operatorId(可选), operatorName(可选) | 催办 |
| POST | /revoke/{id} | id, reason(可选) | 撤回任务(审批人撤回) |
| POST | /follow/{id} | id, userId, userName | 关注任务 |
| DELETE | /follow/{id} | id, userId | 取消关注 |
| GET | /transferHistory/{id} | id | 获取转派历史 |
BpmInstanceController(撤回流程)
基础路径:/api/bpm/instance
| HTTP | 路径 | 参数 | 说明 |
|---|---|---|---|
| POST | /recall/{id} | id, userId | 发起人撤回流程 |
撤回 vs 撤销
- 撤回(recall,
/api/bpm/instance/recall/{id}):发起人操作,仅限无任务完成前,实例状态变为recalled,回写WITHDRAWN。 - 撤回任务(revoke,
/api/bpm/task/revoke/{id}):审批人操作,将任务状态改为revoked,实例状态改为revoked。
操作步骤
1. 发起会签流程
在流程设计器中配置 sign 类型节点,设置 signType(AND/OR)、signRule(ALL/HALF/RATIO)、passRatio。发起流程后引擎自动为每个审批人创建子任务。
2. 会签审批
# 会签审批通过
curl -X POST "http://localhost:52856/api/bpm/sign/approve/1?userId=3&userName=王五&opinion=同意"3. 转办任务
# 转办(记录历史)
curl -X POST "http://localhost:52856/api/bpm/task/transferTask/1" \
-d "fromUserId=2&fromUserName=李四&toUserId=3&toUserName=王五&reason=出差代理&operatorId=2&operatorName=李四"4. 委派任务
# 委派(记录历史)
curl -X POST "http://localhost:52856/api/bpm/task/delegateTask/1" \
-d "fromUserId=2&fromUserName=李四&toUserId=3&toUserName=王五&reason=请代为审核&operatorId=2&operatorName=李四"5. 催办
# 催办审批人
curl -X POST "http://localhost:52856/api/bpm/task/urge/1?operatorId=1&operatorName=张三"6. 加签
# 前加签(在当前审批人之前添加)
curl -X POST "http://localhost:52856/api/bpm/sign/addBefore/1?targetUserId=5&targetUserName=赵六&operatorId=2&operatorName=李四&reason=需要法务审核"7. 关注任务
# 关注任务
curl -X POST "http://localhost:52856/api/bpm/task/follow/1?userId=4&userName=钱七"
# 取消关注
curl -X DELETE "http://localhost:52856/api/bpm/task/follow/1?userId=4"8. 撤回流程
# 发起人撤回
curl -X POST "http://localhost:52856/api/bpm/instance/recall/1?userId=1"常见问题
会签驳回不会等待其他人
当 signType=AND 且有人驳回时,整个实例立即置为 rejected,其他未审批的会签子任务不会自动取消。前端应展示「流程已驳回」状态。
会签进度查询
调用 SimpleFlowEngine.getSignTaskStatus(taskId) 可获取会签进度:
{
"total": 3,
"completed": 2,
"approved": 2,
"rejected": 0,
"pending": 1,
"progress": 66
}转办和委派有两个版本
- 简单版(
/transfer/{id}、/delegate/{id}):直接修改任务,不记录历史 - 完整版(
/transferTask/{id}、/delegateTask/{id}):记录BpmTaskTransferRecord历史,发送通知
生产环境应使用完整版接口。
