YunaiV / ruoyi-vue-pro

🔥 官方推荐 🔥 RuoYi-Vue 全新 Pro 版本,优化重构所有功能。基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 微信小程序,支持 RBAC 动态权限、数据权限、SaaS 多租户、Flowable 工作流、三方登录、支付、短信、商城、CRM、ERP、AI 大模型等功能。你的 ⭐️ Star ⭐️,是作者生发的动力!
https://doc.iocoder.cn/
MIT License
27.98k stars 5.99k forks source link

工作流中支持任务认领 #696

Closed eric-lxm closed 4 days ago

eric-lxm commented 1 week ago

碰到问题,请在 https://github.com/YunaiV/ruoyi-vue-pro/issues 搜索是否存在相似的 issue。

不按照模板提交的 issue,会被系统自动删除。

基本信息

你猜测可能的原因

(必填)我花费了 2-4 小时自查,发现可能的原因是:当一个任务需要多个人处理时,此时增加认领功能,谁认领此任务则可继续处理任务。

复现步骤

第一步,BpmUserTaskActivityBehavior中增加处理多个候选人的逻辑。

(1)计算任务的候选人集合,而非之前单个
private Set<Long> calculateTaskCandidateUsers0(DelegateExecution execution) {
        // 情况一,如果是多实例的任务,例如说会签、或签等情况,则从 Variable 中获取。
        // 顺序审批可见 BpmSequentialMultiInstanceBehavior,并发审批可见 BpmSequentialMultiInstanceBehavior
        if (super.multiInstanceActivityBehavior != null) {
            HashSet<Long> set = new HashSet<>();
            set.add(execution.getVariable(super.multiInstanceActivityBehavior.getCollectionElementVariable(), Long.class));
            return set;
        }

        // 情况二,如果非多实例的任务,则计算任务处理人
        // 第一步,先计算可处理该任务的处理人们
        // 第二步,后随机选择一个任务的处理人
        // 疑问:为什么一定要选择一个任务处理人?
        // 解答:项目对 bpm 的任务是责任到人,所以每个任务有且仅有一个处理人。
        //      如果希望一个任务可以同时被多个人处理,可以考虑使用 BpmParallelMultiInstanceBehavior 实现的会签 or 或签。
        return taskCandidateInvoker.calculateUsers(execution);
    }

(2)为任务设置负责人,支持设置多个负责人
    protected void handleAssignments(TaskService taskService, String assignee, String owner,
                                     List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager,
                                     DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) {
        // 第一步,获得任务的候选用户, 这里可能会有多个候选用户???todo:
        //Long assigneeUserId = calculateTaskCandidateUsers(execution);
        Set<Long> assigneeUserIds = calculateTaskCandidateUsers0(execution);
        Assert.notEmpty(assigneeUserIds, "任务处理人不能为空");

        if (1 < assigneeUserIds.size()) {
            // 第二步,设置作为负责人 todo: 这里随机选择了一个,不合适,需要认领该任务。
            BpmTaskService bpmTaskService = SpringUtil.getBean(BpmTaskService.class);
            bpmTaskService.addCandidateUsers(task.getId(), assigneeUserIds);
        } else if (1 == assigneeUserIds.size()) {
            // 第二步,设置作为负责人
            TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserIds.stream().findFirst().get()));
        }
    }

第二步,获取待办任务时要获取候选和已经分配给的任务。BpmTaskServiceImpl。 (1) 将多个候选人添加到指定任务

/**
     * 添加候选用户 到指定任务
     *
     * @param taskId         任务Id
     * @param candidateUsers 候选用户
     */
    @Override
    public void addCandidateUsers(String taskId, Collection<Long> candidateUsers) {
        candidateUsers.forEach(e -> taskService.addCandidateUser(taskId, String.valueOf(e)));
    }

(2)更改获取待办任务接口。

    public PageResult<Task> getTaskTodoPage0(Long userId, BpmTaskPageReqVO pageVO) {
        TaskQuery taskQuery = taskService.createTaskQuery()
                .taskCandidateOrAssigned(String.valueOf(userId))
                //.taskAssignee(String.valueOf(userId)) // 分配给自己
                .active()
                .includeProcessVariables()
                .orderByTaskCreateTime().desc(); // 创建时间倒序
        if (StrUtil.isNotBlank(pageVO.getName())) {
            taskQuery.taskNameLike("%" + pageVO.getName() + "%");
        }
        if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
            taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
            taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1]));
        }

        List<Task> taskList = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
        long count = taskList.size();
        if (count == 0) {
            return PageResult.empty();
        }

        return new PageResult<>(CollUtil.sub(taskList, 0, (int) count), count);
    }

(2)TaskConvert转换器中。将Task转成BpmTaskRespVO, 将任务认领状态返回前端
default PageResult<BpmTaskRespVO> buildTodoTaskPage0(PageResult<Task> pageResult,
                                                         Map<String, ProcessInstance> processInstanceMap,
                                                         Map<Long, AdminUserRespDTO> userMap) {
        List<BpmTaskRespVO> taskVOList = CollectionUtils.convertList(pageResult.getList(), task -> {
            BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class);
            taskVO.setType(FlowableUtils.getTaskType(task));
            ProcessInstance processInstance = processInstanceMap.get(taskVO.getProcessInstanceId());
            if (processInstance == null) {
                return null;
            }
            taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
            AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
            taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class));
            return taskVO;
        });
        return new PageResult<>(taskVOList, pageResult.getTotal());
    }

(3)增加认领和归还的实现。

/**
     * 认领流程任务
     *
     * @param userId 用户编号
     * @param reqVO  分配请求
     */
    public void claimTask(Long userId, BpmTaskClaimReqVO reqVO) {
        String taskId = reqVO.getId();
        // 1.1 校验任务
        Task task = validateTaskExist(taskId);
        if (StrUtil.isNotBlank(task.getAssignee()) && !task.getAssignee().equals(reqVO.getAssigneeUserId().toString())) { // 校验当前审批人和被转派人不是同一人
            throw exception(TASK_CLAIM_FAIL_TASK_CLAIMED, adminUserApi.getUser(reqVO.getAssigneeUserId()).getNickname());
        }
        // 1.2 校验目标用户存在
        AdminUserRespDTO assigneeUser = adminUserApi.getUser(reqVO.getAssigneeUserId());
        if (assigneeUser == null) {
            throw exception(TASK_CLAIM_FAIL_USER_NOT_EXISTS);
        }

        // 2. 添加认领意见
        taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.CLAIM.getType(),
                BpmCommentTypeEnum.CLAIM.formatComment(assigneeUser.getNickname(), reqVO.getReason()));

        // 3. 认领任务
        taskService.claim(taskId, reqVO.getAssigneeUserId().toString());
    }

    /**
     * 归还流程任务
     *
     * @param userId 用户编号
     * @param reqVO  分配请求
     */
    public void unclaimTask(Long userId, BpmTaskUnclaimReqVO reqVO) {
        String taskId = reqVO.getId();
        // 1.1 校验任务
        Task task = validateTask(userId, reqVO.getId());
        if (!task.getAssignee().equals(reqVO.getAssigneeUserId().toString())) { // 校验当前归还人和任务处理人必须是同一人
            throw exception(TASK_UNCLAIM_FAIL_USER_REPEAT);
        }
        // 1.2 校验目标用户存在
        AdminUserRespDTO assigneeUser = adminUserApi.getUser(reqVO.getAssigneeUserId());
        if (assigneeUser == null) {
            throw exception(TASK_UNCLAIM_FAIL_USER_NOT_EXISTS);
        }

        // 2. 添加归还意见
        taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.CLAIM.getType(),
                BpmCommentTypeEnum.UNCLAIM.formatComment(assigneeUser.getNickname(), reqVO.getReason()));

        // 3. 归还任务
        taskService.unclaim(taskId);
    }

第三步,增加认领和归还接口。BpmTaskController中增加以下接口。

  @PutMapping("/claim")
    @Operation(summary = "认领任务", description = "认领候选组中的指定任务")
    @PreAuthorize("@ss.hasPermission('bpm:task:update')")
    public CommonResult<Boolean> claimTask(@Valid @RequestBody BpmTaskClaimReqVO reqVO) {
        taskService.claimTask(getLoginUserId(), reqVO);
        return success(true);
    }

    @PutMapping("/unclaim")
    @Operation(summary = "归还任务", description = "归还指定任务,任务归还后变成未认领状态")
    @PreAuthorize("@ss.hasPermission('bpm:task:update')")
    public CommonResult<Boolean> unclaimTask(@Valid @RequestBody BpmTaskUnclaimReqVO reqVO) {
        taskService.unclaimTask(getLoginUserId(), reqVO);
        return success(true);
    }

第四步, 判断任务的状态。

public enum TaskType {
    SINGLE_HANDLER(1), // 单个处理人状态
    UNCLAIMED(2),      // 任务处于未认领状态
    CLAIMED(3);        // 任务已经认领

    private final int value;

    TaskType(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

// FlowUtils, 根据任务实体类判断任务状态
public static TaskType getTaskType(Object taskInfo) {
    if (taskInfo instanceof TaskEntityImpl) {
        TaskEntityImpl task = (TaskEntityImpl) taskInfo;
        if (ObjUtil.isEmpty(task.getAssignee())) {
            if (task.getIdentityLinkCount() > 1) {
                return TaskType.UNCLAIMED;
            }
        } else {
            if (task.getIdentityLinkCount() > 1) {
                return TaskType.CLAIMED;
            }
        }
    } else if (taskInfo instanceof HistoricTaskInstanceEntityImpl) {
        HistoricTaskInstanceEntityImpl task = (HistoricTaskInstanceEntityImpl) taskInfo;
        if (ObjUtil.isEmpty(task.getAssignee())) {
            if (task.getIdentityLinks().size() > 1) {
                return TaskType.UNCLAIMED;
            }
        } else {
            if (task.getIdentityLinks().size() > 1) {
                return TaskType.CLAIMED;
            }
        }
    }
    return TaskType.SINGLE_HANDLER;
}

(3)
@Schema(description = "管理后台 - 流程任务 Response VO")
@Data
public class BpmTaskRespVO {

    @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private String id;

    @Schema(description = "任务名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
    private String name;

    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;

    @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime endTime;

    @Schema(description = "持续时间", example = "1000")
    private Long durationInMillis;

    @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
    private Integer status; // 参见 BpmTaskStatusEnum 枚举
    // 任务实体中新增加的任务认领状态字段
    @Schema(description = "任务认领状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private TaskType type = SINGLE_HANDLER; // 默认SINGLE_HANDLER直接分配的任务
}

报错信息

前端获取任务时要携带任务状态,标明任务是处理于待认领状态还是已认领状态。 带上必要的截图

YunaiV commented 4 days ago

🙂 我们目前,暂时不考虑认领这个模式哈。

保持和钉钉、飞书一致。

YunaiV commented 4 days ago

如果想讨论。。。可以等我空点

最近生病 = = 脑子有点转不过来,T T