diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/BpmActivityController.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/BpmActivityController.java new file mode 100644 index 000000000..15931ffaa --- /dev/null +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/BpmActivityController.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.adminserver.modules.bpm.controller.task; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.adminserver.modules.bpm.service.task.BpmActivityService; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Api(tags = "流程活动实例") +@RestController +@RequestMapping("/bpm/activity") +@Validated +public class BpmActivityController { + + @Resource + private BpmActivityService activityService; + + // TODO 芋艿:注解、权限、validtion + + @ApiOperation(value = "生成指定流程实例的高亮流程图", + notes = "只高亮进行中的任务。不过要注意,该接口暂时没用,通过前端的 ProcessViewer.vue 界面的 highlightDiagram 方法生成") + @GetMapping("/generate-highlight-diagram") + @ApiImplicitParam(name = "id", value = "流程实例的编号", required = true, dataTypeClass = String.class) + public void generateHighlightDiagram(@RequestParam("processInstanceId") String processInstanceId, + HttpServletResponse response) throws IOException { + byte[] bytes = activityService.generateHighlightDiagram(processInstanceId); + ServletUtils.writeAttachment(response, StrUtil.format("流程图-{}.svg", processInstanceId), bytes); + } + +} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/BpmTaskController.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/BpmTaskController.java index fdff560d0..5a0b6af4b 100644 --- a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/BpmTaskController.java +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/BpmTaskController.java @@ -20,7 +20,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; -@Api(tags = "流程任务") +@Api(tags = "流程任务实例") @RestController @RequestMapping("/bpm/task") @Validated @@ -72,14 +72,4 @@ public class BpmTaskController { return success(taskService.getTaskListByProcessInstanceId(processInstanceId)); } - /** - * 返回高亮的流转图SVG - * @param processInstanceId 流程Id - */ - @GetMapping("/process/highlight-img") - public void getHighlightImg(@RequestParam String processInstanceId, HttpServletResponse response) throws IOException { - FileResp fileResp = taskService.getHighlightImg(processInstanceId); - ServletUtils.writeAttachment(response, fileResp.getFileName(), fileResp.getFileByte()); - } - } diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/vo/task/FileResp.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/vo/task/FileResp.java deleted file mode 100644 index fb9cbca2c..000000000 --- a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/controller/task/vo/task/FileResp.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.adminserver.modules.bpm.controller.task.vo.task; - -import lombok.Data; - -// TODO @Li:1)改成 HighlightImgRespVO 吧。2)swagger 注解要补充;3)fileByte => fileContent -/** - * 文件输出类 - * - * @author yunlongn - */ -@Data -public class FileResp { - - /** - * 文件名字 - */ - private String fileName; - - /** - * 文件输出流 - */ - private byte[] fileByte; - -} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/enums/BpmErrorCodeConstants.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/enums/BpmErrorCodeConstants.java index ec2cafa7e..6f762f04f 100644 --- a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/enums/BpmErrorCodeConstants.java +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/enums/BpmErrorCodeConstants.java @@ -33,7 +33,8 @@ public interface BpmErrorCodeConstants { ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1009003000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图"); ErrorCode PROCESS_DEFINITION_NAME_NOT_MATCH = new ErrorCode(1009003001, "流程定义的名字期望是({}),当前是({}),请修改 BPMN 流程图"); ErrorCode PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1009003002, "流程定义不存在"); - ErrorCode PROCESS_DEFINITION_IS_SUSPENDED = new ErrorCode(1009003002, "流程定义处于挂起状态"); + ErrorCode PROCESS_DEFINITION_IS_SUSPENDED = new ErrorCode(1009003003, "流程定义处于挂起状态"); + ErrorCode PROCESS_DEFINITION_BPMN_MODEL_NOT_EXISTS = new ErrorCode(1009003004, "流程定义的模型不存在"); // ========== 流程实例 1-009-004-000 ========== ErrorCode PROCESS_INSTANCE_NOT_EXISTS = new ErrorCode(1009004000, "流程实例不存在"); diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/BpmActivityService.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/BpmActivityService.java new file mode 100644 index 000000000..3fa4a5acc --- /dev/null +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/BpmActivityService.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.adminserver.modules.bpm.service.task; + +/** + * BPM 活动实例 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmActivityService { + + /** + * 生成指定流程实例的高亮流程图,只高亮进行中的任务 + * + * 友情提示,非该方法的注释。如果想实现更高级的高亮流程图(当前节点红色 + 完成节点为绿色),可参考如下内容: + * 博客一:https://blog.csdn.net/qq_40109075/article/details/110939639 + * 博客二:https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-flowable/src/main/java/com/ruoyi/flowable/flow/CustomProcessDiagramGenerator.java + * 这里不实现的原理,需要自定义实现 ProcessDiagramGenerator 和 ProcessDiagramCanvas,代码量有点大 + * + * 如果你想实现高亮已完成的任务,可参考 https://blog.csdn.net/qiuxinfa123/article/details/119579863 博客。不过测试下来,貌似不太对~ + * + * @param processInstanceId 实例Id + * @return 图的字节数组 + */ + byte[] generateHighlightDiagram(String processInstanceId); + +} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/BpmTaskService.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/BpmTaskService.java index a063bfeeb..383c124e4 100644 --- a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/BpmTaskService.java +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/BpmTaskService.java @@ -10,7 +10,7 @@ import java.util.List; import java.util.Map; /** - * 流程任务 Service 接口 + * 流程任务实例 Service 接口 * * @author jason * @author 芋道源码 @@ -32,6 +32,14 @@ public interface BpmTaskService { */ List getTaskListByProcessInstanceId(String processInstanceId); + /** + * 获得流程任务列表 + * + * @param processInstanceId 流程实例的编号 + * @return 流程任务列表 + */ + List getTasksByProcessInstanceId(String processInstanceId); + /** * 获得流程任务列表 * @@ -101,13 +109,6 @@ public interface BpmTaskService { */ void rejectTask(Long userId, @Valid BpmTaskRejectReqVO reqVO); - /** - * 返回高亮的流转进程 - * @param processInstanceId 实例Id - * @return {@link FileResp} 返回文件 - */ - FileResp getHighlightImg(String processInstanceId); - // ========== Task 拓展表相关 ========== /** diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/impl/BpmActivityServiceImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/impl/BpmActivityServiceImpl.java new file mode 100644 index 000000000..299b4a980 --- /dev/null +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/impl/BpmActivityServiceImpl.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.adminserver.modules.bpm.service.task.impl; + +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.adminserver.modules.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.adminserver.modules.bpm.service.task.BpmActivityService; +import cn.iocoder.yudao.adminserver.modules.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.adminserver.modules.bpm.service.task.BpmTaskService; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import lombok.extern.slf4j.Slf4j; +import org.activiti.bpmn.model.BpmnModel; +import org.activiti.engine.HistoryService; +import org.activiti.engine.RepositoryService; +import org.activiti.engine.RuntimeService; +import org.activiti.engine.history.HistoricProcessInstance; +import org.activiti.engine.task.Task; +import org.activiti.image.ProcessDiagramGenerator; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.adminserver.modules.bpm.enums.BpmErrorCodeConstants.PROCESS_DEFINITION_BPMN_MODEL_NOT_EXISTS; +import static cn.iocoder.yudao.adminserver.modules.bpm.enums.BpmErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * BPM 活动实例 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +@Validated +public class BpmActivityServiceImpl implements BpmActivityService { + + private static final String FONT_NAME = "宋体"; + + @Resource + private ProcessDiagramGenerator processDiagramGenerator; + + @Resource + private BpmProcessInstanceService processInstanceService; + @Resource + private BpmProcessDefinitionService processDefinitionService; + @Resource + private BpmTaskService taskService; + + @Override + public byte[] generateHighlightDiagram(String processInstanceId) { + // 获得流程实例 + HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId); + if (processInstance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + // 获得流程定义的 BPMN 模型 + BpmnModel bpmnModel = processDefinitionService.getBpmnModel(processInstance.getProcessDefinitionId()); + if (bpmnModel == null) { + throw exception(PROCESS_DEFINITION_BPMN_MODEL_NOT_EXISTS); + } + + // 如果流程已经结束,则无进行中的任务,无法高亮 + // 如果流程未结束,才需要高亮 + List highLightedActivities = Collections.emptyList(); + if (processInstance.getEndTime() == null) { + List tasks = taskService.getTasksByProcessInstanceId(processInstanceId); + highLightedActivities = CollectionUtils.convertList(tasks, Task::getTaskDefinitionKey); + } + + // 生成高亮流程图 + InputStream inputStream = processDiagramGenerator.generateDiagram(bpmnModel, highLightedActivities, Collections.emptyList(), + FONT_NAME, FONT_NAME, FONT_NAME); + return IoUtil.readBytes(inputStream); + } + +} diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/impl/BpmTaskServiceImpl.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/impl/BpmTaskServiceImpl.java index 351ff80fa..e199eba63 100644 --- a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/impl/BpmTaskServiceImpl.java +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/impl/BpmTaskServiceImpl.java @@ -54,7 +54,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** - * 流程任务 Service 实现类 + * 流程任务实例 Service 实现类 * * @author jason * @author 芋道源码 @@ -66,14 +66,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Resource private TaskService taskService; @Resource - private RuntimeService runtimeService; - @Resource private HistoryService historyService; - @Resource - private RepositoryService repositoryService; - - @Resource - private ProcessDiagramGenerator processDiagramGenerator; @Resource private SysUserService userService; @@ -118,6 +111,14 @@ public class BpmTaskServiceImpl implements BpmTaskService { return BpmTaskConvert.INSTANCE.convertList3(tasks, bpmTaskExtDOMap, processInstance, userMap, deptMap); } + @Override + public List getTasksByProcessInstanceId(String processInstanceId) { + if (StrUtil.isEmpty(processInstanceId)) { + return Collections.emptyList(); + } + return taskService.createTaskQuery().processInstanceId(processInstanceId).list(); + } + @Override public List getTasksByProcessInstanceIds(List processInstanceIds) { if (CollUtil.isEmpty(processInstanceIds)) { @@ -270,129 +271,6 @@ public class BpmTaskServiceImpl implements BpmTaskService { // taskService.addComment(task.getId(), task.getProcessInstanceId(), reqVO.getComment()); } - @Override - public FileResp getHighlightImg(String processInstanceId) { - // 查询历史 - //TODO 云扬四海 貌似流程结束后,点击审批进度会报错 - // TODO @Li:一些 historyService 的查询,貌似比较通用,是不是抽一些小方法出来 - HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); - // 如果不存在实例。 说明数据异常 - if (hpi == null) { -// throw exception(PROCESS_INSTANCE_NOT_EXISTS); - throw new RuntimeException("不存在"); - } - // 如果有结束时间 返回model的流程图 - if (!ObjectUtils.isEmpty(hpi.getEndTime())) { - ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().processDefinitionId(hpi.getProcessDefinitionId()).singleResult(); - String resourceName = Optional.ofNullable(pd.getDiagramResourceName()).orElse(pd.getResourceName()); - BpmnModel bpmnModel = repositoryService.getBpmnModel(pd.getId()); - InputStream inputStream = processDiagramGenerator.generateDiagram(bpmnModel, new ArrayList<>(1), new ArrayList<>(1), - "宋体", "宋体", "宋体"); - FileResp fileResp = new FileResp(); - fileResp.setFileName( resourceName + ".svg"); - fileResp.setFileByte(IoUtil.readBytes(inputStream)); - return fileResp; - } - // 没有结束时间。说明流程在执行过程中 - // TODO @Li:一些 runtimeService 的查询,貌似比较通用,是不是抽一些小方法出来 - ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); - - List highLightedActivities = new ArrayList<>(); - // 获取所有活动节点 - List finishedInstances = historyService.createHistoricActivityInstanceQuery() - .processInstanceId(processInstanceId).finished().list(); - finishedInstances.stream().map(HistoricActivityInstance::getActivityId) - .forEach(highLightedActivities::add); - // 已完成的节点+当前节点 - highLightedActivities.addAll(runtimeService.getActiveActivityIds(processInstanceId)); - - BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId()); - // 经过的流 - List highLightedFlowIds = getHighLightedFlows(bpmnModel, processInstanceId); - - //设置"宋体" - try (InputStream inputStream = processDiagramGenerator.generateDiagram(bpmnModel, highLightedActivities, highLightedFlowIds, - "宋体", "宋体", "宋体")){ - FileResp fileResp = new FileResp(); - fileResp.setFileName( hpi.getProcessDefinitionName() + ".svg"); - fileResp.setFileByte(IoUtil.readBytes(inputStream)); - return fileResp; - } catch (IOException e) { - log.error("[getHighlightImg][流程({}) 生成图表失败]", processInstanceId, e); - throw exception(HIGHLIGHT_IMG_ERROR); - } - } - - // TODO @Li:这个方法的可读性还有一定的优化空间,可以思考下哈。 - /** - * 获取指定 processInstanceId 已经高亮的Flows - * 获取已经流转的线 参考: https://blog.csdn.net/qiuxinfa123/article/details/119579863 - * @param bpmnModel model - * @param processInstanceId 流程实例Id - * @return 获取已经流转的列表 - */ - private List getHighLightedFlows(BpmnModel bpmnModel, String processInstanceId) { - // 获取所有的线条 - List historicActivityInstances = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId) - .orderByHistoricActivityInstanceId().asc().list(); - // 高亮流程已发生流转的线id集合 - List highLightedFlowIds = new ArrayList<>(); - // 全部活动节点 - List historicActivityNodes = new ArrayList<>(); - // 已完成的历史活动节点 - List finishedActivityInstances = new ArrayList<>(); - - for (HistoricActivityInstance historicActivityInstance : historicActivityInstances) { - FlowNode flowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstance.getActivityId(), true); - historicActivityNodes.add(flowNode); - // 结束时间不为空,则是已完成节点 - if (historicActivityInstance.getEndTime() != null) { - finishedActivityInstances.add(historicActivityInstance); - } - } - // 提取活动id 是唯一的。塞入Map - Map historicActivityInstanceMap = CollectionUtils.convertMap(historicActivityInstances, HistoricActivityInstance::getActivityId); - // 遍历已完成的活动实例,从每个实例的outgoingFlows中找到已执行的 - for (HistoricActivityInstance currentActivityInstance : finishedActivityInstances) { - // 获得当前活动对应的节点信息及outgoingFlows信息 - FlowNode currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currentActivityInstance.getActivityId(), true); - List sequenceFlows = currentFlowNode.getOutgoingFlows(); - - // 遍历outgoingFlows并找到已流转的 满足如下条件认为已已流转: - // 1.当前节点是并行网关或兼容网关,则通过outgoingFlows能够在历史活动中找到的全部节点均为已流转 - // 2.当前节点是以上两种类型之外的,通过outgoingFlows查找到的时间最早的流转节点视为有效流转 - if (BpmnXMLConstants.ELEMENT_GATEWAY_PARALLEL.equals(currentActivityInstance.getActivityType()) - || BpmnXMLConstants.ELEMENT_GATEWAY_INCLUSIVE.equals(currentActivityInstance.getActivityType())) { - // 遍历历史活动节点,找到匹配流程目标节点的 - for (SequenceFlow sequenceFlow : sequenceFlows) { - FlowNode targetFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(sequenceFlow.getTargetRef(), true); - if (historicActivityNodes.contains(targetFlowNode)) { - highLightedFlowIds.add(targetFlowNode.getId()); - } - } - } else { - long earliestStamp = 0L; - String highLightedFlowId = null; - // 循环流出的流 - for (SequenceFlow sequenceFlow : sequenceFlows) { - HistoricActivityInstance historicActivityInstance = historicActivityInstanceMap.get(sequenceFlow.getTargetRef()); - if (historicActivityInstance == null) { - continue; - } - final long startTime = historicActivityInstance.getStartTime().getTime(); - // 遍历匹配的集合,取得开始时间最早的一个 - if (earliestStamp == 0 || earliestStamp >= startTime) { - highLightedFlowId = sequenceFlow.getId(); - earliestStamp = startTime; - } - } - highLightedFlowIds.add(highLightedFlowId); - } - - } - return highLightedFlowIds; - } - private Task getTask(String id) { return taskService.createTaskQuery().taskId(id).singleResult(); } diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/package-info.java b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/package-info.java new file mode 100644 index 000000000..01ecbf3a2 --- /dev/null +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/bpm/service/task/package-info.java @@ -0,0 +1,12 @@ +/** + * task 包下,存放的都是 xxx 实例。例如说: + * 1. ProcessInstance 是 ProcessDefinition 创建而来的实例; + * 2. TaskInstance 是 TaskDefinition 创建而来的实例; + * 3. ActivityInstance 是 BPMN 流程图的每个元素创建的实例; + * + * 考虑到 Task 和 Activity 可以比较明确表示名字,所以对应的 Service 就没有使用 Instance 后缀~ + * 嘿嘿,其实也是实现到比较后面的阶段,所以就暂时没去统一和修改了~ + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.adminserver.modules.bpm.service.task;