diff --git a/sql/tools/convertor.py b/sql/tools/convertor.py index d286d3b238..b54d1337c8 100644 --- a/sql/tools/convertor.py +++ b/sql/tools/convertor.py @@ -6,12 +6,12 @@ Author: dhb52 (https://gitee.com/dhb52) pip install simple-ddl-parser or with uv -uv run --with simple-ddl-parser convertor.py postgres > ../postgresql/ruoyi-vue-pro.sql 239ms  四 5/22 21:03:16 2025 -uv run --with simple-ddl-parser convertor.py sqlserver > ../sqlserver/ruoyi-vue-pro.sql -uv run --with simple-ddl-parser convertor.py kingbase > ../kingbase/ruoyi-vue-pro.sql -uv run --with simple-ddl-parser convertor.py opengauss > ../opengauss/ruoyi-vue-pro.sql -uv run --with simple-ddl-parser convertor.py oracle > ../oracle/ruoyi-vue-pro.sql -uv run --with simple-ddl-parser convertor.py dm8 > ../dm/ruoyi-vue-pro-dm8.sql +uv run --with simple-ddl-parser convertor.py postgres ../mysql/ruoyi-vue-pro.sql > ../postgresql/ruoyi-vue-pro.sql +uv run --with simple-ddl-parser convertor.py sqlserver ../mysql/ruoyi-vue-pro.sql > ../sqlserver/ruoyi-vue-pro.sql +uv run --with simple-ddl-parser convertor.py kingbase ../mysql/ruoyi-vue-pro.sql > ../kingbase/ruoyi-vue-pro.sql +uv run --with simple-ddl-parser convertor.py opengauss ../mysql/ruoyi-vue-pro.sql > ../opengauss/ruoyi-vue-pro.sql +uv run --with simple-ddl-parser convertor.py oracle ../mysql/ruoyi-vue-pro.sql > ../oracle/ruoyi-vue-pro.sql +uv run --with simple-ddl-parser convertor.py dm8 ../mysql/ruoyi-vue-pro.sql > ../dm/ruoyi-vue-pro-dm8.sql """ import argparse @@ -24,6 +24,9 @@ from typing import Dict, Generator, Optional, Tuple, Union from simple_ddl_parser import DDLParser +# 避免 Windows 系统使用默认的 GBK 编码 +sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1) + PREAMBLE = """/* Yudao Database Transfer Tool @@ -919,9 +922,15 @@ def main(): help="目标数据库类型", choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss"], ) + parser.add_argument( + "path", + type=str, + help="源数据库脚本路径", + default="../mysql/ruoyi-vue-pro.sql" + ) args = parser.parse_args() - sql_file = pathlib.Path("../mysql/ruoyi-vue-pro.sql").resolve().as_posix() + sql_file = pathlib.Path(args.path).resolve().as_posix() convertor = None if args.type == "postgres": convertor = PostgreSQLConvertor(sql_file) diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java index 3929b7106b..460ea78116 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java @@ -60,6 +60,12 @@ public enum DbTypeEnum { * 人大金仓 */ KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"), + + /** + * OceanBase + */ + OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0") + ; public static final Map MAP_BY_NAME = Arrays.stream(values()) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java index 65defd1318..ed91a99f70 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.web.config; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; @@ -7,11 +8,15 @@ import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import com.google.common.collect.Maps; +import jakarta.annotation.Resource; +import jakarta.servlet.Filter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -24,38 +29,57 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -import javax.annotation.Resource; -import javax.servlet.Filter; +import java.util.Map; +import java.util.function.Predicate; @AutoConfiguration @EnableConfigurationProperties(WebProperties.class) -public class YudaoWebAutoConfiguration implements WebMvcConfigurer { +public class YudaoWebAutoConfiguration { - @Resource - private WebProperties webProperties; /** * 应用名 */ @Value("${spring.application.name}") private String applicationName; - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - configurePathMatch(configurer, webProperties.getAdminApi()); - configurePathMatch(configurer, webProperties.getAppApi()); - } + @Bean + public WebMvcRegistrations webMvcRegistrations(WebProperties webProperties) { + return new WebMvcRegistrations() { - /** - * 设置 API 前缀,仅仅匹配 controller 包下的 - * - * @param configurer 配置 - * @param api API 配置 - */ - private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { - AntPathMatcher antPathMatcher = new AntPathMatcher("."); - configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class) - && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包 + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + // 实例化时就带上前缀 + mapping.setPathPrefixes(buildPathPrefixes(webProperties)); + return mapping; + } + + /** + * 构建 prefix → 匹配条件的映射 + */ + private Map>> buildPathPrefixes(WebProperties webProperties) { + AntPathMatcher antPathMatcher = new AntPathMatcher("."); + Map>> pathPrefixes = Maps.newLinkedHashMapWithExpectedSize(2); + putPathPrefix(pathPrefixes, webProperties.getAdminApi(), antPathMatcher); + putPathPrefix(pathPrefixes, webProperties.getAppApi(), antPathMatcher); + return pathPrefixes; + } + + /** + * 设置 API 前缀,仅仅匹配 controller 包下的 + */ + private void putPathPrefix(Map>> pathPrefixes, WebProperties.Api api, AntPathMatcher matcher) { + if (api == null || StrUtil.isEmpty(api.getPrefix())) { + return; + } + pathPrefixes.put(api.getPrefix(), // api 前缀 + clazz -> clazz.isAnnotationPresent(RestController.class) + && matcher.match(api.getController(), clazz.getPackage().getName())); + } + + }; } @Bean diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index 886446d24a..a166bd0905 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java @@ -100,6 +100,10 @@ public class BpmModelMetaInfoVO { @Schema(description = "任务后置通知设置", example = "{}") private HttpRequestSetting taskAfterTriggerSetting; + @Schema(description = "自定义打印模板设置", example = "{}") + @Valid + private PrintTemplateSetting printTemplateSetting; + @Schema(description = "流程 ID 规则") @Data @Valid @@ -180,4 +184,17 @@ public class BpmModelMetaInfoVO { } + @Schema(description = "自定义打印模板设置") + @Data + public static class PrintTemplateSetting { + + @Schema(description = "是否自定义打印模板", example = "false") + @NotNull(message = "是否自定义打印模板不能为空") + private Boolean enable; + + @Schema(description = "打印模板", example = "

") + private String template; + + } + } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java index dd0e910eef..ece20e6022 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; @@ -24,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskInstance; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -34,9 +36,11 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS; @Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请” @RestController @@ -192,8 +196,30 @@ public class BpmProcessInstanceController { @GetMapping("/get-bpmn-model-view") @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") @Parameter(name = "id", description = "流程实例的编号", required = true) - public CommonResult getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) { + public CommonResult getProcessInstanceBpmnModelView( + @RequestParam(value = "id") String id) { return success(processInstanceService.getProcessInstanceBpmnModelView(id)); } + @GetMapping("/get-print-data") + @Operation(summary = "获得流程实例的打印数据") + @Parameter(name = "id", description = "流程实例的编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + public CommonResult getProcessInstancePrintData( + @RequestParam("processInstanceId") String processInstanceId) { + HistoricProcessInstance historicProcessInstance = processInstanceService.getHistoricProcessInstance(processInstanceId); + if (historicProcessInstance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId())); + DeptRespDTO dept = deptApi.getDept(startUser.getDeptId()); + List tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId); + Map userMap = adminUserApi.getUserMap( + convertSet(tasks, item -> Long.valueOf(item.getAssignee()))); + return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePrintData(historicProcessInstance, + processDefinitionService.getProcessDefinitionInfo(historicProcessInstance.getProcessDefinitionId()), + tasks, userMap, + new UserSimpleBaseVO().setNickname(startUser.getNickname()).setDeptName(dept.getName()))); + } + } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessPrintDataRespVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessPrintDataRespVO.java new file mode 100644 index 0000000000..8f85da966a --- /dev/null +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessPrintDataRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 流程实例的打印数据 Response VO") +@Data +public class BpmProcessPrintDataRespVO { + + @Schema(description = "流程实例数据") + private BpmProcessInstanceRespVO processInstance; + + @Schema(description = "是否开启自定义打印模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean printTemplateEnable; + + @Schema(description = "自定义打印模板 HTML") + private String printTemplateHtml; + + @Schema(description = "审批任务列表") + private List tasks; + + @Schema(description = "流程任务") + @Data + public static class Task { + + @Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "签名 URL", example = "https://www.iocoder.cn/sign.png") + private String signPicUrl; + + @Schema(description = "任务描述", requiredMode = Schema.RequiredMode.REQUIRED) + private String description; // 该字段由后端拼接 + + } + +} diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java index f129e5a315..45729f9f22 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; @@ -21,6 +23,10 @@ public class BpmTaskPageReqVO extends PageParam { @Schema(description = "流程定义的标识", example = "2048") private String processDefinitionKey; // 精准匹配 + @Schema(description = "审批状态", example = "1") + @InEnum(BpmTaskStatusEnum.class) + private Integer status; // 仅【已办】使用 + @Schema(description = "创建时间") @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java index 460115286b..90e7753c75 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java @@ -1,23 +1,29 @@ package cn.iocoder.yudao.module.bpm.convert.task; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessPrintDataRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; @@ -35,10 +41,7 @@ import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; import org.mapstruct.factory.Mappers; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -293,4 +296,47 @@ public interface BpmProcessInstanceConvert { .setActivityNodes(activityNodes); } + default BpmProcessPrintDataRespVO buildProcessInstancePrintData(HistoricProcessInstance historicProcessInstance, + BpmProcessDefinitionInfoDO processDefinitionInfo, + List tasks, + Map userMap, + UserSimpleBaseVO startUser) { + BpmModelMetaInfoVO.PrintTemplateSetting printTemplateSetting = processDefinitionInfo.getPrintTemplateSetting(); + BpmProcessPrintDataRespVO printData = new BpmProcessPrintDataRespVO(); + // 打印模板是否开启 + printData.setPrintTemplateEnable(printTemplateSetting != null && Boolean.TRUE.equals(printTemplateSetting.getEnable())); + // 流程相关数据 + BpmProcessInstanceRespVO processInstance = new BpmProcessInstanceRespVO() + .setId(historicProcessInstance.getId()).setName(historicProcessInstance.getName()) + .setBusinessKey(historicProcessInstance.getBusinessKey()) + .setStartTime(DateUtils.of(historicProcessInstance.getStartTime())) + .setEndTime(DateUtils.of(historicProcessInstance.getEndTime())) + .setStartUser(startUser).setStatus(FlowableUtils.getProcessInstanceStatus(historicProcessInstance)) + .setFormVariables(historicProcessInstance.getProcessVariables()) + .setProcessDefinition(BeanUtils.toBean(processDefinitionInfo, BpmProcessDefinitionRespVO.class)); + printData.setProcessInstance(processInstance); + // 审批历史 + List approveTasks = new ArrayList<>(tasks.size()); + tasks.forEach(item -> { + Map taskLocalVariables = item.getTaskLocalVariables(); + BpmProcessPrintDataRespVO.Task approveTask = new BpmProcessPrintDataRespVO.Task(); + approveTask.setName(item.getName()); + approveTask.setId(item.getId()); + approveTask.setSignPicUrl((String) taskLocalVariables.get(BpmnVariableConstants.TASK_SIGN_PIC_URL)); + approveTask.setDescription(StrUtil.format("{} / {} / {} / {} / {}", + userMap.get(Long.valueOf(item.getAssignee())).getNickname(), + item.getName(), + DateUtil.formatDateTime(item.getEndTime()), + BpmTaskStatusEnum.valueOf((Integer) taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_STATUS)).getName(), + taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_REASON))); + approveTasks.add(approveTask); + }); + printData.setTasks(approveTasks); + // 自定义模板 + if (printData.getPrintTemplateEnable() && printTemplateSetting != null) { + printData.setPrintTemplateHtml(printTemplateSetting.getTemplate()); + } + return printData; + } + } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index 37e2c4462d..6fd905ee3d 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java @@ -224,4 +224,10 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting; + /** + * 自定义打印模板设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.PrintTemplateSetting printTemplateSetting; + } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index aeac0876f1..bfd350d642 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -63,7 +63,7 @@ public interface ErrorCodeConstants { ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!"); ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!"); - ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,下一节点不满足撤回条件!"); + ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_020, "撤回失败,下一节点不满足撤回条件!"); // ========== 动态表单模块 1-009-010-000 ========== ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java index 5b42df50fe..87be32633b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.enums.task; +import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import lombok.AllArgsConstructor; @@ -47,4 +48,8 @@ public enum BpmProcessInstanceStatusEnum implements ArrayValuable { APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus()); } + public static BpmProcessInstanceStatusEnum valueOf(Integer status) { + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values()); + } + } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java index 9ba3b5cb3a..df5fd6e634 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java @@ -1,10 +1,14 @@ package cn.iocoder.yudao.module.bpm.enums.task; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.Arrays; + /** * 流程任务 Task 的状态枚举 * @@ -12,7 +16,7 @@ import lombok.Getter; */ @Getter @AllArgsConstructor -public enum BpmTaskStatusEnum { +public enum BpmTaskStatusEnum implements ArrayValuable { SKIP(-2, "跳过"), NOT_START(-1, "未开始"), @@ -35,6 +39,8 @@ public enum BpmTaskStatusEnum { */ WAIT(0, "待审批"); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmTaskStatusEnum::getStatus).toArray(Integer[]::new); + /** * 状态 *

@@ -46,6 +52,11 @@ public enum BpmTaskStatusEnum { */ private final String name; + @Override + public Integer[] array() { + return ARRAYS; + } + public static boolean isRejectStatus(Integer status) { return REJECT.getStatus().equals(status); } @@ -68,4 +79,8 @@ public enum BpmTaskStatusEnum { return ObjUtil.equal(status, CANCEL.getStatus()); } + public static BpmTaskStatusEnum valueOf(Integer status) { + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values()); + } + } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java index 893c4d053c..b3ce946148 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java @@ -43,14 +43,23 @@ public class BpmnVariableConstants { * @see ProcessInstance#getProcessVariables() */ public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID"; + /** - * 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id} + * 流程实例的变量 - 用于判断流程实例变量节点是否驳回:格式 RETURN_FLAG_{节点 id} * - * 目的是:驳回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过 + * 目的是:退回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过 * * @see ProcessInstance#getProcessVariables() */ public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s"; + + /** + * 流程实例的变量前缀 - 用于退回操作,记录需要预测的节点:格式 NEED_SIMULATE_TASK_{节点定义 id} + * + * 目的是:退回操作,预测节点会不准,在流程变量中记录需要预测的节点,来辅助预测 + */ + public static final String PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX = "NEED_SIMULATE_TASK_"; + /** * 流程实例的变量 - 是否跳过表达式 * diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index a3414cedb4..349f88048f 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -658,10 +658,11 @@ public class BpmnModelUtils { // 根据类型,获取入口连线 List sequenceFlows = getElementIncomingFlows(source); + // 1. 没有入口连线,则返回 false if (CollUtil.isEmpty(sequenceFlows)) { - return true; + return false; } - // 循环找到目标元素 + // 2. 循环找目标元素, 找到目标节点 for (SequenceFlow sequenceFlow : sequenceFlows) { // 如果发现连线重复,说明循环了,跳过这个循环 if (visitedElements.contains(sequenceFlow.getId())) { @@ -669,21 +670,22 @@ public class BpmnModelUtils { } // 添加已经走过的连线 visitedElements.add(sequenceFlow.getId()); - // 这条线路存在目标节点,这条线路完成,进入下个线路 + // 这条线路存在目标节点,直接返回 true FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); if (target.getId().equals(sourceFlowElement.getId())) { + return true; + } + // 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?) + if (sourceFlowElement instanceof ParallelGateway) { continue; } - // 如果目标节点为并行网关,则不继续 - if (sourceFlowElement instanceof ParallelGateway) { - return false; - } - // 否则就继续迭代 - if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) { - return false; + // 继续迭代,如果找到目标节点直接返回 true + if (isSequentialReachable(sourceFlowElement, target, visitedElements)) { + return true; } } - return true; + // 未找到返回 false + return false; } /** @@ -783,7 +785,6 @@ public class BpmnModelUtils { return resultElements; } - @SuppressWarnings("PatternVariableCanBeUsed") private static void simulateNextFlowElements(FlowElement currentElement, Map variables, List resultElements, Set visitElements) { // 如果为空,或者已经遍历过,则直接结束 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 80b3038d87..5770ba4455 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; @@ -12,6 +14,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; @@ -72,6 +75,7 @@ import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmA import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum.REJECT_CHILD_PROCESS; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -187,6 +191,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) { processVariables.putAll(reqVO.getProcessVariables()); } + // 特殊:如果是未发起的场景,则设置发起用户,解决“发起流程”时,需要使用到该变量的问题。例如说:https://t.zsxq.com/fMw5g + if (historicProcessInstance == null) { + processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, loginUserId); + } // 1.3 读取其它相关数据 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId() @@ -218,10 +226,24 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3.1 计算当前登录用户的待办任务 BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId()); - // 3.2 预测未运行节点的审批信息 + + // 3.2 获取由于退回操作,需要预测的节点。从流程变量中获取,回退操作会设置这些变量 + Set needSimulateTaskDefKeysByReturn = new HashSet<>(); + if (StrUtil.isNotEmpty(reqVO.getProcessInstanceId())) { + Map variables = runtimeService.getVariables(reqVO.getProcessInstanceId()); + Map simulateTaskVariables = MapUtil.filter(variables, + item -> item.getKey().startsWith(PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX)); + simulateTaskVariables.forEach((key, value) -> + needSimulateTaskDefKeysByReturn.add(StrUtil.removePrefix(key, PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX))); + } + // 移除运行中的节点,运行中的节点无需预测 + // TODO @jason:是不是 foreach runActivityNodes,然后移除 needSimulateTaskDefKeysByReturn 更好?(理解成本低一点) + CollectionUtils.convertList(runActivityNodes, ActivityNode::getId).forEach(needSimulateTaskDefKeysByReturn::remove); + + // 3.3 预测未运行节点的审批信息 List simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, processDefinitionInfo, - processVariables, activities); + processVariables, activities, needSimulateTaskDefKeysByReturn); // 4. 拼接最终数据 return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, @@ -461,7 +483,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } /** - * 获取结束节点的状态 + * 获取结束节点的状态 */ private Integer getEndActivityNodeStatus(HistoricTaskInstance task) { Integer status = FlowableUtils.getTaskStatus(task); @@ -546,7 +568,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService private List getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - List activities) { + List activities, + Set needSimulateTaskDefKeysByReturn) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 Set runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); @@ -555,7 +578,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService List flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables); return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn( startUserId, bpmnModel, flowElements, - processDefinitionInfo, processVariables, flowElement, runActivityIds)); + processDefinitionInfo, processVariables, flowElement, runActivityIds, needSimulateTaskDefKeysByReturn)); } // 情况二:SIMPLE 设计器 if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) { @@ -564,17 +587,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService List simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables); return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple( startUserId, bpmnModel, - processDefinitionInfo, processVariables, simpleNode, runActivityIds)); + processDefinitionInfo, processVariables, simpleNode, runActivityIds, needSimulateTaskDefKeysByReturn)); } throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType()); } private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - BpmSimpleModelNodeVO node, Set runActivityIds) { + BpmSimpleModelNodeVO node, Set runActivityIds, + Set needSimulateTaskDefKeysByReturn) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 - if (runActivityIds.contains(node.getId())) { + if (runActivityIds.contains(node.getId()) + && !needSimulateTaskDefKeysByReturn.contains(node.getId())) { // 特殊:回退操作时候,会记录需要预测的节点到流程变量中。即使在历史操作中,也需要预测 return null; } Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); @@ -621,13 +646,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List flowElements, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - FlowElement node, Set runActivityIds) { - if (runActivityIds.contains(node.getId())) { + FlowElement node, Set runActivityIds, + Set needSimulateTaskDefKeysByReturn) { + // 回退操作时候,会记录需要预测的节点到流程变量中。即使节点在历史操作中,也需要预测。 + if (!needSimulateTaskDefKeysByReturn.contains(node.getId()) && runActivityIds.contains(node.getId())) { return null; } + Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); // 如果节点被跳过,状态设置为跳过 - if(BpmnModelUtils.isSkipNode(node, processVariables)){ + if (BpmnModelUtils.isSkipNode(node, processVariables)) { status = BpmTaskStatusEnum.SKIP.getStatus(); } ActivityNode activityNode = new ActivityNode().setId(node.getId()) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index 00d30adf9c..98615b3539 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -176,6 +176,14 @@ public interface BpmTaskService { */ List getHistoricActivityListByExecutionId(String executionId); + /** + * 获得指定流程实例的已完成的流程任务列表,不包含取消状态 + * + * @param processInstanceId 流程实例的编号 + * @return 流程任务列表 + */ + List getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId); + // ========== Update 写入相关方法 ========== /** diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index e0d2ce9e8d..42c2cc3c20 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.*; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; @@ -68,8 +70,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; -import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG; -import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE; +//import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; /** @@ -230,6 +231,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (StrUtil.isNotBlank(pageVO.getName())) { taskQuery.taskNameLike("%" + pageVO.getName() + "%"); } + if (pageVO.getStatus() != null) { + taskQuery.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, pageVO.getStatus()); + } // if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { // taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); // taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); @@ -491,6 +495,17 @@ public class BpmTaskServiceImpl implements BpmTaskService { return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list(); } + @Override + public List getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId) { + return historyService.createHistoricTaskInstanceQuery() + .finished() + .includeTaskLocalVariables() + .processInstanceId(processInstanceId) + .taskVariableValueNotEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.CANCEL.getStatus()) + .orderByHistoricTaskInstanceStartTime().asc().list(); + } + /** * 判断指定用户,是否是当前任务的审批人 * @@ -590,7 +605,13 @@ public class BpmTaskServiceImpl implements BpmTaskService { bpmnModel, reqVO.getNextAssignees(), instance); runtimeService.setVariables(task.getProcessInstanceId(), variables); - // 5. 调用 BPM complete 去完成任务 + // 5. 移除辅助预测的流程变量,这些变量在回退操作中设置 + // todo @jason:可以直接 + 拼接哈 + String simulateVariableName = StrUtil.concat(false, + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, task.getTaskDefinitionKey()); + runtimeService.removeVariable(task.getProcessInstanceId(), simulateVariableName); + + // 6. 调用 BPM complete 去完成任务 taskService.complete(task.getId(), variables, true); // 【加签专属】处理加签任务 @@ -840,34 +861,34 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (task.isSuspended()) { throw exception(TASK_IS_PENDING); } - // 1.2 校验源头和目标节点的关系,并返回目标元素 - FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(), - reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId()); + // 1.2 获取流程模型信息 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId()); + // 1.3 校验源头和目标节点的关系,并返回目标元素 + FlowElement targetElement = validateTargetTaskCanReturn(bpmnModel, task.getTaskDefinitionKey(), + reqVO.getTargetTaskDefinitionKey()); // 2. 调用 Flowable 框架的退回逻辑 - returnTask(userId, task, targetElement, reqVO); + returnTask(userId, bpmnModel, task, targetElement, reqVO); } /** * 退回流程节点时,校验目标任务节点是否可退回 * - * @param sourceKey 当前任务节点 Key - * @param targetKey 目标任务节点 key - * @param processDefinitionId 当前流程定义 ID + * @param bpmnModel 流程模型 + * @param sourceKey 当前任务节点 Key + * @param targetKey 目标任务节点 key * @return 目标任务节点元素 */ - private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) { - // 1.1 获取流程模型信息 - BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId); - // 1.3 获取当前任务节点元素 + private FlowElement validateTargetTaskCanReturn(BpmnModel bpmnModel, String sourceKey, String targetKey) { + // 1.1 获取当前任务节点元素 FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey); - // 1.3 获取跳转的节点元素 + // 1.2 获取跳转的节点元素 FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey); if (target == null) { throw exception(TASK_TARGET_NODE_NOT_EXISTS); } - // 2.2 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 + // 2. 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 if (!BpmnModelUtils.isSequentialReachable(source, target, null)) { throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR); } @@ -878,11 +899,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { * 执行退回逻辑 * * @param userId 用户编号 + * @param bpmnModel 流程模型 * @param currentTask 当前退回的任务 * @param targetElement 需要退回到的目标任务 * @param reqVO 前端参数封装 */ - public void returnTask(Long userId, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { + public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤 // 1.1 获取所有正常进行的任务节点 Key List taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list(); @@ -915,18 +937,54 @@ public class BpmTaskServiceImpl implements BpmTaskService { } }); - // 3. 设置流程变量节点驳回标记:用于驳回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过 - runtimeService.setVariable(currentTask.getProcessInstanceId(), - String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE); + // 3. 构建需要预测的任务流程变量 + // TODO @jason:【驳回预测相关】是不是搞成一个变量,里面是 set 更简洁一点呀? + Set taskDefinitionKeyList = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement); + Map needSimulateVariables = convertMap(taskDefinitionKeyList, + taskId -> StrUtil.concat(false, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, taskId), item -> Boolean.TRUE); + // 4. 执行驳回 // 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因: // 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944 runtimeService.createChangeActivityStateBuilder() .processInstanceId(currentTask.getProcessInstanceId()) .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey()) + // 设置需要预测的任务流程变量,用于辅助预测 + .processVariables(needSimulateVariables) + // 设置流程变量(local)节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过 + .localVariable(reqVO.getTargetTaskDefinitionKey(), + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), + Boolean.TRUE) .changeState(); } + private Set getNeedSimulateTaskDefinitionKeys(BpmnModel bpmnModel, Task currentTask, FlowElement targetElement) { + // 1. 获取需要预测的任务的 definition key。因为当前任务还没完成,也需要预测 + Set taskDefinitionKeys = CollUtil.newHashSet(currentTask.getTaskDefinitionKey()); + + // 2.1 从已结束任务中找到要回退的目标任务,按时间倒序最近的一个目标任务 + List endTaskList = CollectionUtils.filterList( + getTaskListByProcessInstanceId(currentTask.getProcessInstanceId(), Boolean.FALSE), + item -> item.getEndTime() != null); + // 2.2 遍历已结束的任务,找到在 targetTask 之后生成的任务,且串行可达的任务 + HistoricTaskInstance targetTask = findFirst(endTaskList, + item -> item.getTaskDefinitionKey().equals(targetElement.getId())); + // TODO @jason:【驳回预测相关】是不是 if targetTask 先判空? + endTaskList.forEach(item -> { + FlowElement element = getFlowElementById(bpmnModel, item.getTaskDefinitionKey()); + // 如果已结束的任务在回退目标节点之后生成,且串行可达,则标记为需要预算节点 + // TODO 串行可达的方法需要和判断可回退节点 validateTargetTaskCanReturn 分开吗? 并行网关可能会有问题。 + // TODO @jason:【驳回预测相关】这里是不是判断 element 哈? + if (targetTask != null + // TODO @jason:【驳回预测相关】这里直接 createTime 的 compare 更简单?因为不太会出现空哈。 + && DateUtil.compare(item.getCreateTime(), targetTask.getCreateTime()) > 0 + && BpmnModelUtils.isSequentialReachable(element, targetElement, null)) { + taskDefinitionKeys.add(item.getTaskDefinitionKey()); + } + }); + return taskDefinitionKeys; + } + @Override @Transactional(rollbackFor = Exception.class) public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) { @@ -1438,12 +1496,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { return; } FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); - // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略 - // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识 - Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(), - String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); + // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略(使用 local variable) + Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(), + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(), - PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); if (userTaskElement.getId().equals(START_USER_NODE_ID) && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java index d3282f35a3..25a11104d1 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java @@ -7,6 +7,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 线索分页 Request VO") @Data @@ -42,4 +47,8 @@ public class CrmCluePageReqVO extends PageParam { @Schema(description = "跟进状态", example = "true") private Boolean followUpStatus; + @Schema(description = "创建时间", example = "[2023-01-01 00:00:00, 2023-01-31 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java index 88650dc899..8e52f9a238 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java @@ -33,6 +33,7 @@ public interface CrmClueMapper extends BaseMapperX { .eqIfPresent(CrmClueDO::getLevel, pageReqVO.getLevel()) .eqIfPresent(CrmClueDO::getSource, pageReqVO.getSource()) .eqIfPresent(CrmClueDO::getFollowUpStatus, pageReqVO.getFollowUpStatus()) + .betweenIfPresent(CrmClueDO::getCreateTime, pageReqVO.getCreateTime()) .orderByDesc(CrmClueDO::getId); return selectJoinPage(pageReqVO, CrmClueDO.class, query); } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java index 0ff9e56a9c..7f55507e42 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java @@ -21,9 +21,8 @@ public enum CodegenFrontTypeEnum { VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版 VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版 - // TODO @puhui999::50、51 会好点; - VUE3_VBEN5_EP_SCHEMA(42), // Vue3 VBEN5 + EP + schema 模版 - VUE3_VBEN5_EP_GENERAL(43), // Vue3 VBEN5 + EP 标准模版 + VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版 + VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版 ; /** diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index b3826da2dc..c4ce19ed64 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -343,7 +343,7 @@ public class CodegenEngine { filePath = formatFilePath(filePath, bindingMap); String content = templateEngine.getTemplate(vmPath).render(bindingMap); // 格式化代码 - content = prettyCode(content); + content = prettyCode(content, vmPath); result.put(filePath, content); } @@ -383,11 +383,14 @@ public class CodegenEngine { * 如果不处理,Vue 的 Pretty 格式校验可能会报错 * * @param content 格式化前的代码 + * @param vmPath 模板路径 * @return 格式化后的代码 */ - private String prettyCode(String content) { - // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错 - content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); + private String prettyCode(String content, String vmPath) { + // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5) + if (!StrUtil.contains(vmPath, "vben5")) { + content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); + } // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到 if (StrUtil.count(content, "dateFormatter") == 1) { content = StrUtils.removeLineContains(content, "dateFormatter"); diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm index 90c0a6e9c5..06dc0c86c1 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm @@ -1,12 +1,15 @@