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

相关推荐
独行soc1 小时前
2025年渗透测试面试题总结-某战队红队实习面经(附回答)(题目+回答)
linux·运维·服务器·学习·面试·职场和发展·渗透测试
奋斗者1号2 小时前
神经网络:节点、隐藏层与非线性学习
网络·神经网络·学习
真的想上岸啊3 小时前
学习Linux的第二天
学习
吃货界的硬件攻城狮3 小时前
【STM32 学习笔记】EXTI外部中断
笔记·stm32·学习
吃货界的硬件攻城狮3 小时前
【STM32 学习笔记 】OLED显示屏及Keil调试
笔记·stm32·学习
njsgcs4 小时前
chili3d调试笔记12 deepwiki viewport svg雪碧图 camera three.ts
笔记
非凡ghost4 小时前
NoxLucky:个性化动态桌面,打造独一无二的手机体验
学习·智能手机·软件需求
海尔辛4 小时前
学习黑客 linux 提权
linux·网络·学习
FBI HackerHarry浩4 小时前
Linux云计算训练营笔记day02(Linux、计算机网络、进制)
linux·运维·网络·笔记·计算机网络·进制
烟雨柳成烟4 小时前
Qt学习Day0:Qt简介
开发语言·qt·学习