前言
前文已经完成了候选人的拾取任务,本文再来结合源码梳理实现任务的归还功能。
一、入口
用户调用 taskService.unclaim(taskId)
public void unclaim(String taskId) {
// 直接委托到 claim,传入 userId = null 表示“解绑”
claim(taskId, null);
}
public void claim(String taskId, String userId) {
// 使用命令模式执行解绑/认领
commandExecutor.execute(new ClaimTaskCmd(taskId, userId));
}
二、命令基类加载并校验任务
在执行具体命令前,先检查任务是否存在且未挂起
public abstract class NeedsActiveTaskCmd<T> implements Command<T> {
protected String taskId;
public T execute(CommandContext commandContext) {
TaskEntity task = commandContext
.getTaskEntityManager()
.findById(taskId);
if (task == null) {
throw new FlowableObjectNotFoundException(
"No task found with id " + taskId);
}
if (task.isSuspended()) {
throw new FlowableException(
"Task " + taskId + " is suspended");
}
// 进入具体命令逻辑
return execute(commandContext, task);
}
protected abstract T execute(
CommandContext commandContext,
TaskEntity task);
}
三、执行解绑命令
当 userId == null 时走解绑逻辑
public class ClaimTaskCmd extends NeedsActiveTaskCmd<Void> {
protected String userId; // null 表示解绑
@Override
protected Void execute(
CommandContext commandContext,
TaskEntity task) {
// 解绑:清除受理人
TaskHelper.changeTaskAssignee(task, null);
// 记录历史/事件
HistoryManager.createUserIdentityLinkComment(
task, null, IdentityLinkType.ASSIGNEE);
return null;
}
}
四、真正解绑
删除旧的 ASSIGNEE 链接,设置 assignee 为 null,并发布事件
public static void changeTaskAssignee(
TaskEntity taskEntity,
String assignee) { // 传入 null
// 1. 删除数据库中的 ASSIGNEE 类型 IdentityLink
taskEntity.getIdentityLinkEntityManager()
.deleteIdentityLinksByTaskIdAndType(
taskEntity.getId(),
IdentityLinkType.ASSIGNEE);
// 2. 在内存中清空 assignee 字段
taskEntity.setAssignee(assignee);
// 3. 触发 TASK_ASSIGNED 事件(此时 assignee 为 null)
fireAssignmentEvents(taskEntity);
}
五、持久化与事件/历史
事务内将任务表和身份链接表同步更新,并发布相应事件,写入历史表;TaskEntityManager.update(taskEntity) 刷新到 ACT_RU_TAS;删除操作对应的 DELETE FROM ACT_RU_IDENTITYLINK ...;事件通过 EventDispatcher 分发 ENTITY_LINK_DELETED、TASK_ASSIGNED;若开启历史(historyLevel ≥ AUDIT),会在 ACT_HI_TASKINST 中记录解绑行为。
六、完成后端接口
① 定义请求参数
只需要指定任务ID即可
package com.ceair.entity.request;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author wangbaohai
* @ClassName RevertMyTaskReq
* @description: 归还我的任务请求参数
* @date 2025年05月04日
* @version: 1.0.0
*/
@Data
public class RevertMyTaskReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
// 任务编号
private String taskId;
}
② 定义服务接口
/**
* 归还我的任务
* <p>
* 此方法用于将某个任务归还到其先前的状态通常用于实现任务的归还功能
* 它接受一个包含归还请求详情的对象作为参数,并返回一个布尔值,指示任务是否成功恢复
*
* @param revertMyTaskReq 恢复任务请求对象,包含需要恢复的任务的相关信息
* @return Boolean 如果任务成功恢复,则返回true;否则返回false
*/
Boolean revertMyTask(RevertMyTaskReq revertMyTaskReq);
③ 实现服务接口
核心就是使用【taskService.unclaim()】API实现归还的功能。
/**
* 归还我的任务
* <p>
* 此方法接收一个 RevertMyTaskReq 对象作为参数,该对象包含任务ID,
* 方法使用此ID通过 taskService 将任务归还到未分配状态
*
* @param revertMyTaskReq 包含任务ID的请求对象,用于指定要归还的任务
* @return 如果任务成功归还,返回 true;否则抛出异常
* @throws BusinessException 当参数非法或业务逻辑出现问题时抛出此异常
*/
@Override
public Boolean revertMyTask(RevertMyTaskReq revertMyTaskReq) {
try {
// 参数判空
if (revertMyTaskReq == null || StringUtils.isBlank(revertMyTaskReq.getTaskId())) {
log.error("归还任务失败:非法的任务ID");
throw new IllegalArgumentException("归还任务失败:非法的任务ID");
}
// 通过 taskService 归还任务
taskService.unclaim(revertMyTaskReq.getTaskId());
return true;
} catch (IllegalArgumentException e) {
log.error("归还任务失败,原因:参数错误", e);
throw new BusinessException("归还任务失败,原因:参数错误", e);
} catch (BusinessException e) {
log.error("归还任务失败,原因:业务异常", e);
throw new BusinessException("归还任务失败,原因:业务异常", e);
} catch (Exception e) {
log.error("归还任务失败,原因:未知异常", e);
throw new BusinessException("归还任务失败,原因:未知异常", e);
}
}
④ 定义功能接口
/**
* 任务归还。
* <p>
* 权限: /api/v1/myTask/revertMyTask
* 参数: revertMyTaskReq - 包含任务归还相关信息的请求对象
* 返回: Result<Boolean> 表示任务归还是否成功
* <p>
* 异常处理:
* - 业务层异常 返回任务归还失败信息
* - 其他未知异常 系统异常提示
*/
@PreAuthorize("hasAnyAuthority('/api/v1/myTask/revertMyTask')")
@Parameter(name = "revertMyTaskReq", description = "任务归还请求对象", required = true)
@Operation(summary = "任务归还")
@PostMapping("/revertMyTask")
public Result<Boolean> revertMyTask(@RequestBody RevertMyTaskReq revertMyTaskReq) {
try {
// 调用业务层方法,将任务分配给当前登录用户
return Result.success(mayTaskService.revertMyTask(revertMyTaskReq));
} catch (Exception e) {
log.error("任务归还失败,原因:{}", e.getMessage());
return Result.error("任务归还失败,原因:" + e.getMessage());
}
}
七、完善前端按钮功能
① 定义前端参数类型
// 归还任务请求参数
export interface RevertMyTaskReq {
taskId: string // 任务编号,对应 Java 中的 String taskId
}
② 封装请求接口
/**
* 归还任务
*/
export function returnMyTask(data: RevertMyTaskReq) {
return request.post<any>({
url: '/pm-process/api/v1/myTask/revertMyTask',
data,
})
}
③ 优化界面按钮
<el-button v-if="scope.row.status === 2" v-hasButton="`btn.myTask.revertMyTask`" type="primary" @click="onRevert(scope.row)">
归还
</el-button>
④ 完成按钮功能
/**
* 异步函数:用于归还任务
* 当用户需要撤销当前任务时调用此函数
* @param row 任务对象,包含任务ID等信息
*/
async function onRevert(row: TaskVO) {
try {
// 获取当前任务ID并设置参数
const param: RevertMyTaskReq = {
taskId: row.taskId,
}
// 调用后端接口进行归还操作
const result: any = await returnMyTask(param)
// 如果接口调用成功且返回的状态码为200,则显示成功提示信息
if (result.success && result.code === 200) {
ElMessage({
message: '归还成功',
type: 'success',
})
// 重新加载数据
handerPageData()
}
else {
// 如果归还失败,显示错误信息
ElMessage({
message: `归还失败: ${result.message}`,
type: 'error',
})
}
}
catch (error) {
// 捕获异常并提取错误信息
let errorMessage = '未知错误'
if (error instanceof Error) {
errorMessage = error.message
}
// 显示操作失败的错误提示信息
ElMessage({
message: `归还失败: ${errorMessage || '未知错误'}`,
type: 'error',
})
}
}
八、添加权限
① 创建按钮
② 角色分配按钮
九、验证功能
① 定义流程
我们作为候选人参与审批流程
② 发布流程
③ 启动流程
启动的动态参数指定我们自己的账号
④ 先拾取任务
⑤ 最后归还任务
可以看到归还成功后,此任务重新进入了拾取池中,可以被候选组重新拾取了
后记
至此我们对任务的审批主要操作都差不多完成了,此外我们还需要查看当前流程到底走到哪一步了,需要把审批中的流程图输出出来,我们下一篇文章来完成一下。
本文的后端分支是 process-10
本文的前端分支是 process-12