Browse Source

feat(flow): 优化多实例会签任务处理逻辑

- 实现多实例会签任务的正确统计和分页查询功能
- 添加批量提交流程任务接口支持
- 新增FlowTaskDto数据传输对象用于批量操作
- 优化任务指派和用户显示名的获取逻辑
- 修复多实例任务在候选人可见性上的重复计算问题
feature/2026-07-ccc-dev
chenchuchuan 3 weeks ago
parent
commit
83d7acdbb6
  1. 118
      common/common-flow-online/src/main/java/apelet/common/flow/online/controller/FlowOnlineOperationController.java
  2. 55
      common/common-flow/src/main/java/apelet/common/flow/dto/FlowTaskDto.java
  3. 111
      common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java
  4. 12
      common/common-flow/src/main/java/apelet/common/flow/vo/FlowTaskVo.java

118
common/common-flow-online/src/main/java/apelet/common/flow/online/controller/FlowOnlineOperationController.java

@ -4,9 +4,15 @@ import apelet.common.core.annotation.DisableDataFilter; @@ -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; @@ -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; @@ -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 { @@ -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<Void> submitBatch(
@MyRequestBody(required = true) List<FlowTaskDto> 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<TaskInfoVo> taskInfoResult = flowOperationHelper.verifyAndGetRuntimeTaskInfo(task);
if (!taskInfoResult.isSuccess()) {
return ResponseResult.errorFrom(taskInfoResult);
}
TaskInfoVo taskInfo = taskInfoResult.getData();
// 验证在线表单及其关联数据源的合法性。
ResponseResult<OnlineDatasource> 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<Void> 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<Map<OnlineDatasourceRelation, List<JSONObject>>> 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();
}
}

55
common/common-flow/src/main/java/apelet/common/flow/dto/FlowTaskDto.java

@ -0,0 +1,55 @@ @@ -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和idtype的值参考FlowConstant中的常量值
*/
@Schema(description = "传阅公共数据,格式为type和id,type的值参考FlowConstant中的常量值")
private JSONObject copyData;
}

111
common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java

@ -699,10 +699,12 @@ public class FlowApiServiceImpl implements FlowApiService { @@ -699,10 +699,12 @@ public class FlowApiServiceImpl implements FlowApiService {
query.taskNameLike("%" + taskName + "%");
}
this.buildCandidateCondition(query, username);
long totalCount = query.count();
query.orderByTaskCreateTime().desc();
Map<String, Boolean> multiInstanceTaskMap = new HashMap<>();
long totalCount = this.countOwnedTaskList(query, username, multiInstanceTaskMap);
int firstResult = (pageParam.getPageNum() - 1) * pageParam.getPageSize();
List<Task> taskList = query.listPage(firstResult, pageParam.getPageSize());
List<Task> taskList = this.listOwnedTaskPage(
query, username, firstResult, pageParam.getPageSize(), multiInstanceTaskMap);
return new MyPageData<>(taskList, totalCount);
}
@ -710,7 +712,102 @@ public class FlowApiServiceImpl implements FlowApiService { @@ -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<String, Boolean> multiInstanceTaskMap) {
long totalCount = 0;
int cursor = 0;
while (true) {
List<Task> 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<Task> listOwnedTaskPage(
TaskQuery query, String username, int firstResult, int pageSize, Map<String, Boolean> multiInstanceTaskMap) {
List<Task> resultList = new LinkedList<>();
int skipCount = firstResult;
int cursor = 0;
while (resultList.size() < pageSize) {
List<Task> 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<String, Boolean> 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<String, Boolean> 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 { @@ -807,6 +904,7 @@ public class FlowApiServiceImpl implements FlowApiService {
workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c));
List<FlowTaskExt> allFlowTaskExtList = flowTaskExtService.getByProcessDefinitionId(processDefinitionIdSet);
Map<String, FlowTaskExt> flowTaskExtMap = allFlowTaskExtList.stream().collect(Collectors.toMap(t -> t.getProcessDefinitionId() + ":" + t.getTaskId(), t -> t, (k1, k2) -> k1));
Map<String, Boolean> multiInstanceTaskMap = new HashMap<>();
for (Task task : taskList) {
FlowTaskVo flowTaskVo = new FlowTaskVo();
flowTaskVo.setTaskId(task.getId());
@ -814,6 +912,7 @@ public class FlowApiServiceImpl implements FlowApiService { @@ -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 { @@ -841,6 +940,8 @@ public class FlowApiServiceImpl implements FlowApiService {
}
Set<String> loginNameSet = flowTaskVoList.stream()
.map(FlowTaskVo::getProcessInstanceInitiator).collect(Collectors.toSet());
loginNameSet.addAll(flowTaskVoList.stream()
.map(FlowTaskVo::getAssignee).filter(StrUtil::isNotBlank).collect(Collectors.toSet()));
List<FlowUserInfoVo> flowUserInfos = flowCustomExtFactory
.getFlowIdentityExtHelper().getUserInfoListByUsernameSet(loginNameSet);
Map<String, FlowUserInfoVo> userInfoMap =
@ -849,6 +950,10 @@ public class FlowApiServiceImpl implements FlowApiService { @@ -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;
}

12
common/common-flow/src/main/java/apelet/common/flow/vo/FlowTaskVo.java

@ -128,4 +128,16 @@ public class FlowTaskVo { @@ -128,4 +128,16 @@ public class FlowTaskVo {
*/
@Schema(description = "解析后的任务标题")
private String title;
/**
* 任务指派人审批人
*/
@Schema(description = "任务指派人(审批人)")
private String assignee;
/**
* 任务指派人审批人显示名
*/
@Schema(description = "任务指派人(审批人)显示名")
private String assigneeShowName;
}

Loading…
Cancel
Save