From 83d7acdbb602aa53830c0ce774ca185eb0ef322e Mon Sep 17 00:00:00 2001 From: chenchuchuan <13554600537@163.com> Date: Tue, 11 Aug 2026 11:42:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(flow):=20=E4=BC=98=E5=8C=96=E5=A4=9A?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E4=BC=9A=E7=AD=BE=E4=BB=BB=E5=8A=A1=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现多实例会签任务的正确统计和分页查询功能 - 添加批量提交流程任务接口支持 - 新增FlowTaskDto数据传输对象用于批量操作 - 优化任务指派和用户显示名的获取逻辑 - 修复多实例任务在候选人可见性上的重复计算问题 --- .../controller/FlowOnlineOperationController.java | 118 ++++++++++++++++++++- .../java/apelet/common/flow/dto/FlowTaskDto.java | 55 ++++++++++ .../flow/service/impl/FlowApiServiceImpl.java | 111 ++++++++++++++++++- .../java/apelet/common/flow/vo/FlowTaskVo.java | 12 +++ 4 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 common/common-flow/src/main/java/apelet/common/flow/dto/FlowTaskDto.java diff --git a/common/common-flow-online/src/main/java/apelet/common/flow/online/controller/FlowOnlineOperationController.java b/common/common-flow-online/src/main/java/apelet/common/flow/online/controller/FlowOnlineOperationController.java index 2c5070f..679542c 100644 --- a/common/common-flow-online/src/main/java/apelet/common/flow/online/controller/FlowOnlineOperationController.java +++ b/common/common-flow-online/src/main/java/apelet/common/flow/online/controller/FlowOnlineOperationController.java @@ -4,9 +4,15 @@ import apelet.common.core.annotation.DisableDataFilter; import apelet.common.core.annotation.MyRequestBody; import apelet.common.core.constant.ErrorCodeEnum; import apelet.common.core.object.*; -import apelet.common.core.util.*; -import apelet.common.flow.constant.*; +import apelet.common.core.util.MyCommonUtil; +import apelet.common.core.util.MyModelUtil; +import apelet.common.core.util.MyPageUtil; +import apelet.common.flow.constant.FlowApprovalType; +import apelet.common.flow.constant.FlowConstant; +import apelet.common.flow.constant.FlowTaskStatus; +import apelet.common.flow.constant.FlowTaskType; import apelet.common.flow.dto.FlowTaskCommentDto; +import apelet.common.flow.dto.FlowTaskDto; import apelet.common.flow.dto.FlowWorkOrderDto; import apelet.common.flow.exception.FlowOperationException; import apelet.common.flow.model.*; @@ -15,7 +21,10 @@ import apelet.common.flow.online.service.FlowEntryOnlineService; import apelet.common.flow.online.service.FlowOnlineOperationService; import apelet.common.flow.service.*; import apelet.common.flow.util.FlowOperationHelper; -import apelet.common.flow.vo.*; +import apelet.common.flow.vo.FlowEntryVo; +import apelet.common.flow.vo.FlowTaskVo; +import apelet.common.flow.vo.FlowWorkOrderVo; +import apelet.common.flow.vo.TaskInfoVo; import apelet.common.generator.utils.OrmGenDataSourceUtil; import apelet.common.log.annotation.OperationLog; import apelet.common.log.model.constant.SysOperationLogType; @@ -44,8 +53,8 @@ import com.github.pagehelper.page.PageMethod; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; @@ -1434,4 +1443,105 @@ public class FlowOnlineOperationController { } return null; } + + /** + * 批量提交流程的用户任务。 + * 该接口无需数据权限过滤,因此用DisableDataFilter注解标注。如果当前系统没有支持数据权限过滤,该注解不会有任何影响。 + * + * @param flowTaskDtos 批量提交统一请求DTO,承载全部请求参数 + * @param flowTaskCommentDto 流程审批数据。 + * @return 应答结果对象。 + */ + @DisableDataFilter + @OperationLog(type = SysOperationLogType.SUBMIT_TASK) + @PostMapping("/submitBatch") + public ResponseResult submitBatch( + @MyRequestBody(required = true) List flowTaskDtos, + @MyRequestBody(required = true) FlowTaskCommentDto flowTaskCommentDto) { + + FlowTaskComment flowTaskComment = BeanUtil.copyProperties(flowTaskCommentDto, FlowTaskComment.class); + + // 循环处理每一条流程任务 + for (FlowTaskDto flowTaskDto : flowTaskDtos) { + String processInstanceId = flowTaskDto.getProcessInstanceId(); + String taskId = flowTaskDto.getTaskId(); + String errorMessage; + + // 验证流程任务的合法性。 + Task task; + try { + task = flowApiService.getProcessInstanceActiveTask(processInstanceId, taskId); + } catch (Exception e) { + log.error("批量提交-获取任务失败 processInstanceId:{}, taskId:{}", processInstanceId, taskId, e); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, + "流程实例[" + processInstanceId + "]任务[" + taskId + "]不存在或已结束"); + } + + ResponseResult taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task); + if (!taskInfoResult.isSuccess()) { + return ResponseResult.errorFrom(taskInfoResult); + } + TaskInfoVo taskInfo = taskInfoResult.getData(); + + // 验证在线表单及其关联数据源的合法性。 + ResponseResult datasourceResult = this.verifyAndGetOnlineDatasource(taskInfo.getFormId()); + if (!datasourceResult.isSuccess()) { + return ResponseResult.errorFrom(datasourceResult); + } + + CallResult assigneeVerifyResult = flowApiService.verifyAssigneeOrCandidateAndClaim(task); + if (!assigneeVerifyResult.isSuccess()) { + return ResponseResult.errorFrom(assigneeVerifyResult); + } + OnlineDatasource datasource = datasourceResult.getData(); + ProcessInstance instance = flowApiService.getProcessInstance(processInstanceId); + String dataId = instance.getBusinessKey(); + + // 从当前循环FlowTaskDto获取本条任务独立数据 ========== + JSONObject itemTaskVar = flowTaskDto.getTaskVariableData(); + JSONObject itemMasterData = flowTaskDto.getMasterData(); + JSONObject itemSlaveData = flowTaskDto.getSlaveData(); + JSONObject itemCopyData = flowTaskDto.getCopyData(); + + // 拷贝变量,防止多条任务互相污染变量对象 + JSONObject currentTaskVariableData = itemTaskVar == null ? new JSONObject() : new JSONObject(itemTaskVar); + // 这里把传阅数据放到任务变量中 + if (MapUtil.isNotEmpty(itemCopyData)) { + currentTaskVariableData.put(FlowConstant.COPY_DATA_KEY, itemCopyData); + } + + if (StrUtil.isBlank(dataId)) { + ResponseResult submitNewTaskResult = this.submitNewTask(processInstanceId, taskId, + flowTaskComment, currentTaskVariableData, datasource, itemMasterData, itemSlaveData); + if (!submitNewTaskResult.isSuccess()) { + return submitNewTaskResult; + } + continue; + } + + try { + if (StrUtil.equals(flowTaskComment.getApprovalType(), FlowApprovalType.TRANSFER) + && StrUtil.isBlank(flowTaskComment.getDelegateAssignee())) { + errorMessage = "数据验证失败,加签或转办任务指派人不能为空!!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + // 如果本次请求中包含从表数据,则一同插入。 + ResponseResult>> slaveDataListResult = + onlineOperationHelper.buildSlaveDataList(datasource.getDatasourceId(), itemSlaveData); + if (!slaveDataListResult.isSuccess()) { + return ResponseResult.errorFrom(slaveDataListResult); + } + + flowOnlineOperationService.updateAndTakeTask( + task, flowTaskComment, currentTaskVariableData, datasource, itemMasterData, dataId, slaveDataListResult.getData()); + } catch (FlowOperationException e) { + log.error("批量提交-执行任务失败 processInstanceId:{}, taskId:{}", processInstanceId, taskId, e); + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, + "流程实例[" + processInstanceId + "]任务[" + taskId + "]处理失败:" + e.getMessage()); + } + } + + // 全部任务处理完成 + return ResponseResult.success(); + } } diff --git a/common/common-flow/src/main/java/apelet/common/flow/dto/FlowTaskDto.java b/common/common-flow/src/main/java/apelet/common/flow/dto/FlowTaskDto.java new file mode 100644 index 0000000..46d7403 --- /dev/null +++ b/common/common-flow/src/main/java/apelet/common/flow/dto/FlowTaskDto.java @@ -0,0 +1,55 @@ +package apelet.common.flow.dto; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 批量提交流程任务 + * + * @author guifc + * @date 2023-08-04 + */ +@Schema(description = "批量提交流程任务") +@Data +public class FlowTaskDto { + + /** + * 流程标识 + */ + @Schema(description = "流程标识") + @NotNull(message = "数据验证失败,任务的流程标识不能为空!") + private String processInstanceId; + + /** + * 流程任务ID + */ + @Schema(description = "流程任务ID") + @NotBlank(message = "数据验证失败,任务ID不能为空!") + private String taskId; + + /** + * 流程任务公共变量数据 + */ + @Schema(description = "流程任务公共变量数据") + private JSONObject taskVariableData; + /** + * 流程审批相关公共主表数据 + */ + @Schema(description = "流程审批相关公共主表数据") + private JSONObject masterData; + /** + * 流程审批相关公共从表数据 + */ + @Schema(description = "流程审批相关公共从表数据") + private JSONObject slaveData; + /** + * 传阅公共数据,格式为type和id,type的值参考FlowConstant中的常量值 + */ + @Schema(description = "传阅公共数据,格式为type和id,type的值参考FlowConstant中的常量值") + private JSONObject copyData; + +} \ No newline at end of file diff --git a/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java b/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java index 89af9d2..8f780d2 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java +++ b/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java @@ -699,10 +699,12 @@ public class FlowApiServiceImpl implements FlowApiService { query.taskNameLike("%" + taskName + "%"); } this.buildCandidateCondition(query, username); - long totalCount = query.count(); query.orderByTaskCreateTime().desc(); + Map multiInstanceTaskMap = new HashMap<>(); + long totalCount = this.countOwnedTaskList(query, username, multiInstanceTaskMap); int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize(); - List taskList = query.listPage(firstResult, pageParam.getPageSize()); + List taskList = this.listOwnedTaskPage( + query, username, firstResult, pageParam.getPageSize(), multiInstanceTaskMap); return new MyPageData<>(taskList, totalCount); } @@ -710,7 +712,102 @@ public class FlowApiServiceImpl implements FlowApiService { public long getTaskCountByUserName(String username) { TaskQuery query = this.createQuery(); this.buildCandidateCondition(query, username); - return query.count(); + return this.countOwnedTaskList(query, username, new HashMap<>()); + } + + /** + * 统计当前用户待办任务数。 + * 多实例会签节点在流程图中将全部会签人同时配置为候选者,导致每个实例对所有人可见。 + * 这里只保留会签实例归属人自己的那一条,其他会签人的实例不参与统计。 + */ + private long countOwnedTaskList(TaskQuery query, String username, Map multiInstanceTaskMap) { + long totalCount = 0; + int cursor = 0; + while (true) { + List batch = query.listPage(cursor, 500); + if (CollUtil.isEmpty(batch)) { + break; + } + cursor += batch.size(); + for (Task task : batch) { + if (this.isTaskOwnedByUser(task, username, multiInstanceTaskMap)) { + totalCount++; + } + } + } + return totalCount; + } + + /** + * 分页获取当前用户待办任务。 + * 多实例会签节点的实例对候选者全体可见,分页时跳过不属于当前用户的其他会签实例。 + */ + private List listOwnedTaskPage( + TaskQuery query, String username, int firstResult, int pageSize, Map multiInstanceTaskMap) { + List resultList = new LinkedList<>(); + int skipCount = firstResult; + int cursor = 0; + while (resultList.size() < pageSize) { + List batch = query.listPage(cursor, Math.max(pageSize, 500)); + if (CollUtil.isEmpty(batch)) { + break; + } + cursor += batch.size(); + for (Task task : batch) { + if (!this.isTaskOwnedByUser(task, username, multiInstanceTaskMap)) { + continue; + } + if (skipCount > 0) { + skipCount--; + continue; + } + resultList.add(task); + if (resultList.size() >= pageSize) { + break; + } + } + } + return resultList; + } + + /** + * 判断任务是否属于当前用户。 + * 多实例会签实例的元素变量 assignee 即该实例真正的归属人。 + */ + private boolean isTaskOwnedByUser(Task task, String username, Map multiInstanceTaskMap) { + String taskKey = task.getProcessDefinitionId() + ":" + task.getTaskDefinitionKey(); + Boolean isMultiInstance = multiInstanceTaskMap.get(taskKey); + if (isMultiInstance == null) { + isMultiInstance = this.isMultiInstanceTask(task.getProcessDefinitionId(), task.getTaskDefinitionKey()); + multiInstanceTaskMap.put(taskKey, isMultiInstance); + } + if (!isMultiInstance) { + return true; + } + Object assignee = taskService.getVariable(task.getId(), "assignee"); + return assignee == null || StrUtil.equals(assignee.toString(), username); + } + + /** + * 获取任务的审批人。 + * 多实例会签实例的指派人保存在元素变量 assignee 中,普通任务取任务的指派人。 + */ + private String getTaskAssignee(Task task, Map multiInstanceTaskMap) { + String assignee = task.getAssignee(); + if (StrUtil.isNotBlank(assignee)) { + return assignee; + } + String taskKey = task.getProcessDefinitionId() + ":" + task.getTaskDefinitionKey(); + Boolean isMultiInstance = multiInstanceTaskMap.get(taskKey); + if (isMultiInstance == null) { + isMultiInstance = this.isMultiInstanceTask(task.getProcessDefinitionId(), task.getTaskDefinitionKey()); + multiInstanceTaskMap.put(taskKey, isMultiInstance); + } + if (!isMultiInstance) { + return null; + } + Object assigneeVar = taskService.getVariable(task.getId(), "assignee"); + return assigneeVar == null ? null : assigneeVar.toString(); } @Override @@ -807,6 +904,7 @@ public class FlowApiServiceImpl implements FlowApiService { workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); List allFlowTaskExtList = flowTaskExtService.getByProcessDefinitionId(processDefinitionIdSet); Map flowTaskExtMap = allFlowTaskExtList.stream().collect(Collectors.toMap(t -> t.getProcessDefinitionId() + ":" + t.getTaskId(), t -> t, (k1, k2) -> k1)); + Map multiInstanceTaskMap = new HashMap<>(); for (Task task : taskList) { FlowTaskVo flowTaskVo = new FlowTaskVo(); flowTaskVo.setTaskId(task.getId()); @@ -814,6 +912,7 @@ public class FlowApiServiceImpl implements FlowApiService { flowTaskVo.setTaskKey(task.getTaskDefinitionKey()); flowTaskVo.setTaskFormKey(task.getFormKey()); flowTaskVo.setTaskStartTime(task.getCreateTime()); + flowTaskVo.setAssignee(this.getTaskAssignee(task, multiInstanceTaskMap)); flowTaskVo.setEntryId(flowEntryPublishMap.get(task.getProcessDefinitionId()).getEntryId()); ProcessDefinition processDefinition = definitionMap.get(task.getProcessDefinitionId()); flowTaskVo.setProcessDefinitionId(processDefinition.getId()); @@ -841,6 +940,8 @@ public class FlowApiServiceImpl implements FlowApiService { } Set loginNameSet = flowTaskVoList.stream() .map(FlowTaskVo::getProcessInstanceInitiator).collect(Collectors.toSet()); + loginNameSet.addAll(flowTaskVoList.stream() + .map(FlowTaskVo::getAssignee).filter(StrUtil::isNotBlank).collect(Collectors.toSet())); List flowUserInfos = flowCustomExtFactory .getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet); Map userInfoMap = @@ -849,6 +950,10 @@ public class FlowApiServiceImpl implements FlowApiService { FlowUserInfoVo userInfo = userInfoMap.get(flowTaskVo.getProcessInstanceInitiator()); flowTaskVo.setShowName(userInfo.getShowName()); flowTaskVo.setHeadImageUrl(userInfo.getHeadImageUrl()); + FlowUserInfoVo assigneeInfo = userInfoMap.get(flowTaskVo.getAssignee()); + if (assigneeInfo != null) { + flowTaskVo.setAssigneeShowName(assigneeInfo.getShowName()); + } } return flowTaskVoList; } diff --git a/common/common-flow/src/main/java/apelet/common/flow/vo/FlowTaskVo.java b/common/common-flow/src/main/java/apelet/common/flow/vo/FlowTaskVo.java index 3a40a3b..b9545e6 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/vo/FlowTaskVo.java +++ b/common/common-flow/src/main/java/apelet/common/flow/vo/FlowTaskVo.java @@ -128,4 +128,16 @@ public class FlowTaskVo { */ @Schema(description = "解析后的任务标题") private String title; + + /** + * 任务指派人(审批人)。 + */ + @Schema(description = "任务指派人(审批人)") + private String assignee; + + /** + * 任务指派人(审批人)显示名。 + */ + @Schema(description = "任务指派人(审批人)显示名") + private String assigneeShowName; }