Flowable7.x学习笔记(十八)拾取我的待办

前言

本文从解读源码到实现功能,完整的学习Flowable的【TaskService】-【claim】方法实现的任务拾取功能。

一、概述

当调用 TaskService.claim(taskId, userId) 时,Flowable 会先加载并校验任务实体,再判断该任务是否已被认领;若未被认领,则将 assignee 字段设为传入的 userId(null 则表示"解绑"),并在身份链接(IdentityLink)表中做相应的增删操作;随后触发任务分配事件、写入历史记录(如开启历史配置时),最后将更新结果刷新到数据库。所有这些操作最终都委托给了 TaskServiceImpl 中的 ClaimTaskCmd 来完成。

二、方法签名与定位

claim(String taskId, String userId) 方法在 org.flowable.engine.TaskService 接口中定义,由 TaskServiceImpl 实现,实现层直接调用命令执行器(commandExecutor.execute(new ClaimTaskCmd(taskId, userId))),ClaimTaskCmd 是执行认领逻辑的核心。

ClaimTaskCmd 继承自 NeedsActiveTaskCmd,用于确保任务处于激活状态后再执行认领逻辑。

public class ClaimTaskCmd extends NeedsActiveTaskCmd {

protected String userId;

public ClaimTaskCmd(String taskId, String userId) {

super(taskId);

this.userId = userId;

}

}

构造器保存了要认领的 taskId 和 userId,并由父类处理挂起状态检查,当传入的 userId 非空时,表示要将任务分配给某位用户。

① 设置认领时间与状态

使用引擎的 Clock 获取当前时间,调用 task.setClaimTime(...) 和 task.setClaimedBy(userId) 设置认领元数据。将任务状态更新为 Task.CLAIMED。

② 冲突检查

若该任务已被分配给某人(task.getAssignee()!=null),且与当前 userId 不同,则抛出 FlowableTaskAlreadyClaimedException,避免多用户并发认领。

若已分配给同一用户,则仅记录一次 recordTaskInfoChange(...),保持历史一致性。

③ 首次分配

当任务尚未有 assignee 时,调用 TaskHelper.changeTaskAssignee(task, userId) 为任务设置新认领者。

如果配置了 UserTaskStateInterceptor,会触发其 handleClaim(...) 回调,用于外部扩展。

④ 写入身份链接历史

最后,无论是首次分配还是重复分配,都通过 HistoryManager.createUserIdentityLinkComment(...) 将 ASSIGNEE 类型的身份链接(认领记录)写入历史审计表。

三、加载与校验任务

① 加载任务实体

命令中首先通过 TaskEntityManager.findById(taskId) 从 ACT_RU_TASK 表中获取 TaskEntity。

② 任务存在性检查

若返回 null,抛出 FlowableObjectNotFoundException,提示"无此任务"。

③ 已认领冲突检查

若 task.getAssignee() 不为 null 且与传入的 userId 不同,则抛出冲突异常(FlowableConflictException),禁止不同用户重复认领.

四、赋值 assignee 与解绑

① 设为认领

当 userId 非空时,调用 TaskEntity.setAssignee(userId) 将任务归属新认领者。

② 设为解绑

当 userId==null 时,等价于 unclaim 操作,相当于将 assignee 设回 null。

五、身份链接(IdentityLink)处理

① 历史遗留机制

Flowable 中"候选人"与"认领人"都存储在 ACT_RU_IDENTITYLINK 表,但 ASSIGNEE 类型在表中常伴随特殊处理(空 TASK_ID 或 PROC_INSTANCE_ID 字段)。

② 若是认领

在命令里会先 删除 原有与该任务相关的 ASSIGNEE 类型 IdentityLink,再 新增 一条新的 IdentityLinkType.ASSIGNEE,以确保数据一致。

③ 若是解绑

则只执行删除操作,不新增。

六、事件分发与历史记录

① 事件分发

完成认领后,Flowable 会通过 EventDispatcher 发布 ENTITY_LINK_CREATED(或删除时 ENTITY_LINK_DELETED)以及 TASK_ASSIGNED 等事件,供监听器或审计插件消费。

② 历史记录

若引擎配置 historyLevel ≥ AUDIT,会在 ACT_HI_TASKINST 表中记录任务认领时间、认领者等信息。

七、持久化更新

最后,命令通过 TaskEntityManager.update(taskEntity) 将变更刷回 ACT_RU_TASK 表,并同步提交事务。若配置了异步历史,历史记录写入也可能延迟到异步作业中完成,进一步提升性能。

八、完成后端接口

① 定义请求参数

这里我们只需要传入任务ID即可,用户ID我们从框架的session中获取,一般的脚手架都是这样的,请结合自己的脚手架处理。

复制代码
package com.ceair.entity.request;

import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

/**
 * @author wangbaohai
 * @ClassName PickupMyTaskReq
 * @description: 拾取我的任务请求参数
 * @date 2025年05月03日
 * @version: 1.0.0
 */
@Data
public class PickupMyTaskReq implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    // 任务编号
    private String taskId;

}

② 定义服务接口

复制代码
/**
 * 领取我的任务接口
 * 此方法允许用户领取属于自己的任务,根据提供的条件和参数
 *
 * @param pickupMyTaskReq 领取任务的请求对象,包含领取任务所需的信息和条件
 * @return 返回一个Boolean值,表示任务领取是否成功true表示成功,false表示失败
 */
Boolean pickupMyTask(PickupMyTaskReq pickupMyTaskReq);

③ 实现服务接口

复制代码
/**
 * 拾取我的待办任务
 * <p>
 * 此方法允许当前登录用户拾取一个待办任务通过提供任务ID和当前用户信息,
 * 系统将该任务分配给当前用户
 *
 * @param pickupMyTaskReq 包含任务ID的请求对象如果请求对象或任务ID为空,
 *                        将抛出IllegalArgumentException异常
 * @return 任务拾取成功返回true,否则抛出异常
 * @throws BusinessException        如果用户未登录或任务拾取过程中发生业务异常
 * @throws IllegalArgumentException 如果输入参数不合法,如任务ID为空
 */
@Override
public Boolean pickupMyTask(PickupMyTaskReq pickupMyTaskReq) {
    try {
        // 参数判空
        if (pickupMyTaskReq == null || StringUtils.isBlank(pickupMyTaskReq.getTaskId())) {
            log.error("任务拾取失败:非法的任务ID");
            throw new IllegalArgumentException("任务拾取失败:非法的任务ID");
        }

        // 获取当前登录用户信息
        UserInfo userInfo = userInfoUtils.getUserInfoFromAuthentication();
        if (userInfo == null) {
            log.error("拾取我的待办任务失败,原因:用户未登录");
            throw new BusinessException("拾取我的待办任务失败,原因:用户未登录");
        }

        // 通过 taskService 拾取任务
        taskService.claim(pickupMyTaskReq.getTaskId(), String.valueOf(userInfo.getId()));

        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/pickupMyTask
 * 参数: pickupMyTaskReq - 包含任务拾取相关信息的请求对象
 * 返回: Result<Boolean> 表示任务拾取是否成功
 * <p>
 * 异常处理:
 * - 业务层异常  返回任务拾取失败信息
 * - 其他未知异常  系统异常提示
 */
@PreAuthorize("hasAnyAuthority('/api/v1/myTask/pickupMyTask')")
@Parameter(name = "pickupMyTaskReq", description = "任务拾取请求对象", required = true)
@Operation(summary = "任务拾取")
@PostMapping("/pickupMyTask")
public Result<Boolean> pickupMyTask(@RequestBody PickupMyTaskReq pickupMyTaskReq) {
    try {
        // 调用业务层方法,将任务分配给当前登录用户
        return Result.success(mayTaskService.pickupMyTask(pickupMyTaskReq));
    } catch (Exception e) {
        log.error("任务拾取失败,原因:{}", e.getMessage());
        return Result.error("任务拾取失败,原因:" + e.getMessage());
    }
}

九、完善前端功能按钮

① 定义前端参数类型

// 拾取任务请求参数

export interface PickupMyTaskReq {

taskId: string // 任务编号,对应 Java 中的 String taskId

}

② 封装请求接口

/**

* 拾取任务

*/

export function pickupMyTask(data: PickupMyTaskReq) {

return request.post<any>({

url: '/pm-process/api/v1/myTask/pickupMyTask',

data,

})

}

③ 优化界面按钮

这里把拾取按钮加上权限自定义指令,以及点击事件功能

<el-button v-if="scope.row.status === 1" v-hasButton="`btn.myTask.pickupMyTask`" type="primary" @click="onPickup(scope.row)">

拾取

</el-button>

④ 完成按钮功能

复制代码
/**
 * 异步函数用于处理任务拾取操作
 * @param row 任务对象,包含任务ID等信息
 */
async function onPickup(row: TaskVO) {
  try {
    // 获取当前任务ID并设置参数
    const param: PickupMyTaskReq = {
      taskId: row.taskId,
    }

    // 调用后端接口进行拾取操作
    const result: any = await pickupMyTask(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

相关推荐
我爱挣钱我也要早睡!18 小时前
Java 复习笔记
java·开发语言·笔记
知识分享小能手20 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
汇能感知1 天前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun1 天前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao1 天前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾1 天前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT1 天前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa1 天前
HTML和CSS学习
前端·css·学习·html
ST.J1 天前
前端笔记2025
前端·javascript·css·vue.js·笔记
Suckerbin1 天前
LAMPSecurity: CTF5靶场渗透
笔记·安全·web安全·网络安全