flowable候选人及候选人组(Candidate Users 、Candidate Groups)的应用包含拾取、归还、交接

核心概念:

候选人(Candidate Users )和候选人组(Candidate Groups)是flowable中的一个核心概念,是实现灵活、动态任务分配的核心机制,在了解候选人和候选人组之前,我们需要了解flowable中的几个核心概念:

  1. 用户任务(User Task): BPMN 中的一种活动类型,代表需要人工参与和完成的任务。

  2. 任务分配(Task Assignment): 指定哪个或哪些用户有权限查看、认领(Claim)和完成(Complete)某个用户任务。

  3. 候选人(Candidate Users): 在任务创建时,预先指定一个或多个具体的用户ID 。这些用户(以及他们所属的组,见下一点)将成为该任务的潜在处理者。他们能看到任务,可以认领任务使其成为自己的"待办"(Assignee),然后完成任务。

  4. 候选人组(Candidate Groups): 在任务创建时,预先指定一个或多个组ID (通常是代表角色、部门、团队的标识符,如 "managers", "hr-department", "team-lead")。属于这些组的所有用户将成为该任务的潜在处理者。与候选人用户一样,他们能看到任务,可以认领并完成任务。

  5. 认领(Claim): 当一个候选用户(无论是直接指定的还是通过组继承的)决定处理某个任务时,需要执行"认领"操作。认领后,该任务从"候选任务池"中移出,变成该用户的个人待办任务(Assignee Task),通常其他用户就无法再认领了(除非释放或重新分配)。

  6. 办理人(Assignee): 任务被认领后,任务会有一个具体的办理人(Assignee)。这个办理人负责最终完成任务。任务在创建时可以直接指定办理人(跳过候选步骤),也可以通过候选人/组机制让用户认领后成为办理人。

关键区别与关系:

  • 候选人 vs. 办理人 (Assignee):

    • 候选人是潜在的办理人(可能有多个)。

    • 办理人是实际处理任务的人(通常只有一个)。

    • 任务创建时,可以只设候选人/组不设办理人 (需要认领),也可以直接设办理人(无需认领)。

  • 候选人用户 vs. 候选人组:

    • 候选人用户:直接指定具体个人

    • 候选人组:指定一个角色或团队,间接指定了属于该组的所有用户。

    • 一个任务可以同时设置候选人用户和候选人组。 最终的任务候选人是这两部分的并集。例如,任务设置了候选人用户 userA 和候选人组 managers,那么 userA 和所有属于 managers 组的用户(包括 userA 如果也在组内)都是候选人。

  • 可见性: 设置了候选人/组的任务,会出现在这些用户的 "候选任务列表" 中(通常通过 taskService.createTaskQuery().taskCandidateUser(userId).taskCandidateGroup(groupId) 查询)。直接指定了办理人的任务,只出现在该办理人的 "待办任务列表" 中(通过 .taskAssignee(userId) 查询)。

为什么需要候选人/组机制?

  1. 解耦流程定义与具体人员:

    • 流程设计时,你无法预知流程运行时具体由哪个张三李四处理任务(人员可能变动、请假等)。

    • 使用候选人组(如 "部门经理"),可以将任务分配给一个角色,而不是具体人。实际运行时,由属于该角色的用户来处理。流程定义更稳定,无需因人员变动而修改流程图。

  2. 实现任务池(Task Pool)或工作组(Work Group):

    • 对于某些任务,可能适合由一组人中的任意一个来处理(例如,客服团队处理客户咨询,开发团队处理Bug)。

    • 将任务分配给候选人组(如 "customer-service", "dev-team-alpha"),该组内的所有成员都能看到任务,谁有空谁就可以认领处理。提高了任务处理的灵活性和效率。

  3. 多部门/角色协作:

    • 一个任务可能需要来自不同部门或具备不同角色的用户共同关注或处理(可能由其中一人认领)。

    • 同时设置多个候选人组(如 "finance", "legal"),让财务部和法务部的人都能看到这个合同审批任务,由相关部门的人认领处理。

  4. 指定备选人 + 角色:

    • 除了分配给角色(组),可能还需要额外指定几个关键人员(如专家、备份人员)作为候选人用户,确保他们也能看到任务。

使用场景:

  1. 请假审批(角色分配):

    • BPMN 设计: 在"部门经理审批"用户任务上,设置 候选人组 = "dept-manager:{applicantDept}" (动态组,根据申请人部门变量 applicantDept 决定,如 "dept-manager:sales")。

    • 运行时: 任务创建时,引擎根据变量 applicantDept(如 "sales")解析出候选人组 "dept-manager:sales"

    • 效果: 销售部的所有经理都能在候选任务列表中看到这个请假申请任务。其中一位经理认领后成为办理人并审批。

  2. 客户服务工单(工作组/任务池):

    • BPMN 设计: 在"处理客户问题"用户任务上,设置 候选人组 = "customer-support-tier1"

    • 运行时: 所有属于 Tier 1 客服组的成员都能看到新创建的工单任务。

    • 效果: 第一个有空闲的 Tier 1 客服认领该工单进行处理。

  3. 复杂审批(多角色关注 + 指定专家):

    • BPMN 设计: 在"技术方案评审"用户任务上,设置:

      • 候选人组 = "engineering-leads", "product-managers"

      • 候选人用户 = "expert-architect-john"

    • 运行时: 所有工程主管、产品经理以及专家 John 都能看到这个评审任务。

    • 效果: 通常由一位工程主管或产品经理认领主导评审,但专家 John 和其他角色的人也能看到任务详情,必要时可参与讨论或提供意见(认领机制确保最终由一人负责完成)。

  4. 直接分配(知道具体负责人):

    • BPMN 设计: 在"张三编写报告"用户任务上,直接设置办理人 (Assignee) = "zhangsan" (或通过变量 ${reportOwner} 动态指定)。

    • 运行时: 任务创建时直接分配给用户 zhangsan

    • 效果: 任务只出现在张三的待办列表中,他直接处理无需认领。(这不是候选人/组机制,但作为对比)

在flowable中设置候选人/组:

主要有三种方法:

1.在BPMN2,0 XML中定义(设计时):
XML 复制代码
<userTask id="managerApproval" name="部门经理审批" flowable:candidateGroups="${approvalGroup}" flowable:candidateUsers="${approvalUsers}"> <!-- 动态候选人组名 -->  <!-- 动态候选人 --> 
</userTask>

<userTask id="managerApproval" name="部门经理审批" 
flowable:candidateGroups="managers" flowable:candidateUsers="manager1,manager2"> <!-- 静态组名 -->  <!-- 静态候选人 --> 
</userTask>
2.运行时API设置(启动流程或完成任务时):
java 复制代码
// 1. 启动流程时设置任务候选人/组 (需要知道任务定义Key)
Map<String, Object> variables = new HashMap<>();
variables.put("dynamicGroup", "sales-managers"); // 动态组变量

// 使用 TaskDefinition 设置 (推荐,清晰)
Map<String, Object> startupTaskVariables = new HashMap<>();
startupTaskVariables.put("_FLOWABLE_CANDIDATE_USERS", Arrays.asList("kermit", "fozzie")); // 候选人用户列表
startupTaskVariables.put("_FLOWABLE_CANDIDATE_GROUPS", Arrays.asList("management", "${dynamicGroup}")); // 候选人组列表 (支持表达式)

runtimeService.startProcessInstanceByKey(
    "myProcess",
    businessKey,
    variables, // 普通流程变量
    startupTaskVariables // 专门用于初始化任务变量的Map
);

// 2. 在完成上一个任务时设置下一个任务的候选人/组 (在TaskListener或Delegate中)
taskService.complete(taskId, variables); // 普通方式,需要在BPMN中配置或监听器里提前设置

// 或者在 ExecutionListener/TaskListener 中设置
public void notify(DelegateTask delegateTask) {
    if ("managerApproval".equals(delegateTask.getTaskDefinitionKey())) {
        // 直接设置
        delegateTask.addCandidateUser("kermit");
        delegateTask.addCandidateGroup("management");
        delegateTask.addCandidateGroup((String) delegateTask.getVariable("dynamicGroup"));

        // 或者使用变量 (需在BPMN中配置对应属性)
        // delegateTask.setVariableLocal("_FLOWABLE_CANDIDATE_USERS", Arrays.asList("kermit"));
    }
}
3.任务创建后修改(运行时管理):
java 复制代码
// 获取任务
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

// 添加候选人用户
taskService.addCandidateUser(taskId, "newCandidateUser");

// 添加候选人组
taskService.addCandidateGroup(taskId, "newCandidateGroup");

// 删除候选人用户
taskService.deleteCandidateUser(taskId, "oldCandidateUser");

// 删除候选人组
taskService.deleteCandidateGroup(taskId, "oldCandidateGroup");

// 注意:也可以完全替换,但通常先删除旧的再添加新的更安全,或者使用 set 操作(Flowable API 可能没有直接的 setCandidates 方法,需组合操作)。

候选组/人之间的操作:

查询某个任务实例的候选任务:
java 复制代码
// 1. 查询某个任务实例的所有候选任务 (包括通过用户本身和用户所属组)
List<Task> candidateTasksForUser = taskService.createTaskQuery()
    .proceaaInstanceId("xxx")
    //.processDefinitionKey("ppp:xx:xx")
    .taskCandidateUser("张三") // 当前登录用户
    .list();


// 2. 查询属于某个特定组的所有候选任务
List<Task> candidateTasksForGroup = taskService.createTaskQuery()
    .proceaaInstanceId("xxx")
    //.processDefinitionKey("ppp:xx:xx")
    .taskCandidateGroup(groupId) // groupId 如 "managers"
    .list();
拾取任务:
java 复制代码
Task candidateTasksForUser = taskService.createTaskQuery()
    .proceaaInstanceId("xxx")
    //.processDefinitionKey("ppp:xx:xx")
    .taskCandidateUser("张三") // 当前登录用户
    .singleResult();

if(candidateTasksForUser != null){
    //拾取对应任务
    taskService.claim(candidateTasksForUser.getId,"张三");
}

拾取后,该候选人就会成为该任务的assginee,其他候选人将不能再查询到该任务

归还任务:
java 复制代码
Task candidateTasksForUser = taskService.createTaskQuery()
    .proceaaInstanceId("xxx")
    //.processDefinitionKey("ppp:xx:xx")
    .taskAssignee("张三") // 当前登录用户
    .singleResult();

if(candidateTasksForUser != null){
    //归还对应任务
    taskService.unclaim(candidateTasksForUser.getId);
}
任务交接:
java 复制代码
Task candidateTasksForUser = taskService.createTaskQuery()
    .proceaaInstanceId("xxx")
    //.processDefinitionKey("ppp:xx:xx")
    .taskAssignee("张三") // 当前登录用户
    .singleResult();

if(candidateTasksForUser != null){
    //交接对应任务
    taskService.setAssignee(candidateTasksForUser.getId,"李四");
}

注意:在代码示例中使用.singleResult()来收集查询的数据并进行任务的拾取归还交接,但是在实际业务中这是存在问题的:

1.一个流程实例通常包含多个任务节点,用户可能在同一个流程实例中有多个候选任务

2.一个用户可能属于多个候选人群,在不同节点都有候选资格

3.业务上允许一个流程实例中存在多个待处理任务

4.如果查询返回多个结果 ,此代码会抛出 FlowableException
org.flowable.common.engine.api.FlowableException: Query return 2 results instead of max 1

5.当存在多个候选任务时,用户可能想选择特定任务认领 。代码只认领第一个找到的任务,忽略其他可能更重要的任务

正确做法:使用List处理多个任务

java 复制代码
// 正确:获取流程实例中用户的所有候选任务(返回列表)
List<Task> candidateTasks = taskService.createTaskQuery()
    .processInstanceId("xxx")
    .taskCandidateUser("张三")
    .orderByTaskCreateTime().asc() // 推荐添加排序
    .list(); // 关键:使用 list() 而不是 singleResult()

if (!candidateTasks.isEmpty()) {
    // 场景1:自动认领所有任务(罕见需求)
    for (Task task : candidateTasks) {
        taskService.claim(task.getId(), "张三");
    }
    
    // 场景2:让用户选择特定任务认领(常见)
    // 通常需要将任务列表返回前端供用户选择
}
相关推荐
WispX8882 分钟前
【手写系列】手写 AQS 实现 MyLock
java·开发语言·并发·aqs··手写·lock
初叶 crmeb8 分钟前
JAVA单商户易联云小票打印替换模板
java·linux·python
RainbowSea14 分钟前
秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven-5
java·spring boot·分布式
汤姆yu24 分钟前
基于springboot的民间文化艺术品销售系统
java·spring boot·后端
二进制小甜豆37 分钟前
Spring MVC
java·后端·spring·mvc
Yvonne9781 小时前
定时任务:springboot集成xxl-job-core(一)
java·spring boot·xxl-job
泰勒疯狂展开1 小时前
Java研学-MongoDB(一)
java·开发语言·mongodb
大数据魔法师1 小时前
MongoDB(七) - MongoDB副本集安装与配置
数据库·mongodb
red_redemption1 小时前
JSP、HTML和Tomcat
java·tomcat·html
李斯维1 小时前
循序渐进 Android Binder(一):IPC 基本概念和 AIDL 跨进程通信的简单实例
android·java·android studio