From cd82edbaf6a202be6906adf72a3c660c35a41047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Fri, 7 Aug 2026 16:08:18 +0800 Subject: [PATCH 01/12] =?UTF-8?q?chore(build):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E4=B8=BA=E5=BF=AB=E7=85=A7=E5=B9=B6?= =?UTF-8?q?=E7=A6=81=E7=94=A8BladeX=E4=BB=93=E5=BA=93=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 492c624..41c2daa 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ - 4.10.0.RELEASE + 4.10.0.BASE-SNAPSHOT 17 3.14.1 @@ -350,11 +350,11 @@ false - + @@ -367,7 +367,7 @@ - + From 1e1a817081420eb8947286535812aebb63bb9dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Fri, 7 Aug 2026 18:57:28 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat(launcher):=20=E6=B7=BB=E5=8A=A0Nacos?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=90=AF=E7=94=A8=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/launch/LauncherServiceImpl.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/blade-common/src/main/java/org/springblade/common/launch/LauncherServiceImpl.java b/blade-common/src/main/java/org/springblade/common/launch/LauncherServiceImpl.java index 97deb52..d50ff5e 100644 --- a/blade-common/src/main/java/org/springblade/common/launch/LauncherServiceImpl.java +++ b/blade-common/src/main/java/org/springblade/common/launch/LauncherServiceImpl.java @@ -27,6 +27,7 @@ package org.springblade.common.launch; import org.springblade.common.constant.LauncherConstant; import org.springblade.core.auto.service.AutoService; +import org.springblade.core.launch.BladeApplication; import org.springblade.core.launch.service.LauncherService; import org.springblade.core.launch.utils.PropsUtil; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -45,14 +46,17 @@ public class LauncherServiceImpl implements LauncherService { public void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev) { Properties props = System.getProperties(); - // nacos注册中心配置 - PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.username", LauncherConstant.NACOS_USERNAME); - PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.password", LauncherConstant.NACOS_PASSWORD); - PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.server-addr", LauncherConstant.nacosAddr(profile)); - // nacos配置中心配置 - PropsUtil.setProperty(props, "spring.cloud.nacos.config.username", LauncherConstant.NACOS_USERNAME); - PropsUtil.setProperty(props, "spring.cloud.nacos.config.password", LauncherConstant.NACOS_PASSWORD); - PropsUtil.setProperty(props, "spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile)); + if (BladeApplication.isNacosConfigEnabled()) { + // nacos注册中心配置 + PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.username", LauncherConstant.NACOS_USERNAME); + PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.password", LauncherConstant.NACOS_PASSWORD); + PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.server-addr", LauncherConstant.nacosAddr(profile)); + // nacos配置中心配置 + PropsUtil.setProperty(props, "spring.cloud.nacos.config.username", LauncherConstant.NACOS_USERNAME); + PropsUtil.setProperty(props, "spring.cloud.nacos.config.password", LauncherConstant.NACOS_PASSWORD); + PropsUtil.setProperty(props, "spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile)); + } + // sentinel配置 PropsUtil.setProperty(props, "spring.cloud.sentinel.transport.dashboard", LauncherConstant.sentinelAddr(profile)); // 多数据源配置 From f692d8e275bae89da1322888baa3897d9a38fac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Fri, 7 Aug 2026 19:05:15 +0800 Subject: [PATCH 03/12] =?UTF-8?q?refactor(auth):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=9C=8D=E5=8A=A1=E9=85=8D=E7=BD=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/springblade/auth/AuthApplication.java | 2 ++ .../src/main/resources/application-dev.yml | 15 ---------- .../src/main/resources/application-prod.yml | 15 ---------- .../src/main/resources/application-test.yml | 15 ---------- blade-auth/src/main/resources/application.yml | 28 ++++++++++++++++++- 5 files changed, 29 insertions(+), 46 deletions(-) delete mode 100644 blade-auth/src/main/resources/application-dev.yml delete mode 100644 blade-auth/src/main/resources/application-prod.yml delete mode 100644 blade-auth/src/main/resources/application-test.yml diff --git a/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java b/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java index c674021..cafd777 100644 --- a/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java +++ b/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java @@ -43,6 +43,8 @@ import org.springframework.session.data.redis.config.annotation.web.http.EnableR public class AuthApplication { public static void main(String[] args) { + // 禁用框架注入的nacos import配置、config、discovery 配置 + BladeApplication.disableNacosLaunchConfig(); BladeApplication.run(AppConstant.APPLICATION_AUTH_NAME, AuthApplication.class, args); } diff --git a/blade-auth/src/main/resources/application-dev.yml b/blade-auth/src/main/resources/application-dev.yml deleted file mode 100644 index 25bafbc..0000000 --- a/blade-auth/src/main/resources/application-dev.yml +++ /dev/null @@ -1,15 +0,0 @@ -#服务器端口 -server: - port: 8100 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.dev.url} - username: ${blade.datasource.dev.username} - password: ${blade.datasource.dev.password} - -#第三方登陆 -social: - enabled: true - domain: http://127.0.0.1:2888 diff --git a/blade-auth/src/main/resources/application-prod.yml b/blade-auth/src/main/resources/application-prod.yml deleted file mode 100644 index dc6f80c..0000000 --- a/blade-auth/src/main/resources/application-prod.yml +++ /dev/null @@ -1,15 +0,0 @@ -#服务器端口 -server: - port: 8100 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.prod.url} - username: ${blade.datasource.prod.username} - password: ${blade.datasource.prod.password} - -#第三方登陆 -social: - enabled: true - domain: http://127.0.0.1:2888 diff --git a/blade-auth/src/main/resources/application-test.yml b/blade-auth/src/main/resources/application-test.yml deleted file mode 100644 index c7c6c40..0000000 --- a/blade-auth/src/main/resources/application-test.yml +++ /dev/null @@ -1,15 +0,0 @@ -#服务器端口 -server: - port: 8100 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.test.url} - username: ${blade.datasource.test.username} - password: ${blade.datasource.test.password} - -#第三方登陆 -social: - enabled: true - domain: http://127.0.0.1:2888 diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index 9e7573c..d81e0c6 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -1,6 +1,30 @@ -# 在使用Spring默认数据源Hikari的情况下配置以下配置项 +#服务器端口 +server: + port: 8100 spring: + application: + name: blade-auth + config: + import: + - nacos:blade.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + cloud: + nacos: + username: ${NACOS_USERNAME:nacos} + password: ${NACOS_PASSWORD:nacos} + server-addr: ${NACOS_HOST:127.0.0.1:8848} + discovery: + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + config: + # 文件后缀名 + file-extension: yaml + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} datasource: + url: ${blade.datasource.${spring.profiles.active}.url} + username: ${blade.datasource.${spring.profiles.active}.username} + password: ${blade.datasource.${spring.profiles.active}.password} + # 在使用Spring默认数据源Hikari的情况下配置以下配置项 hikari: # 自动提交从池中返回的连接 auto-commit: true @@ -42,6 +66,8 @@ swagger: #第三方登陆 social: + enabled: true + domain: http://127.0.0.1:2888 oauth: GITHUB: client-id: 233************ From 31f8e07ffebf9e9cba7f21ff1e7cc84ed874d93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Fri, 7 Aug 2026 19:08:43 +0800 Subject: [PATCH 04/12] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0MK?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E7=AB=AF=E7=82=B9=E5=92=8C=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/pom.xml | 4 ++ .../auth/endpoint/OAuth2MKEndpoint.java | 68 +++++++++++++++++++ .../exception/OAuth2MKExceptionHandler.java | 13 ++++ blade-auth/src/main/resources/application.yml | 1 + 4 files changed, 86 insertions(+) create mode 100644 blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2MKEndpoint.java create mode 100644 blade-auth/src/main/java/org/springblade/auth/exception/OAuth2MKExceptionHandler.java diff --git a/blade-auth/pom.xml b/blade-auth/pom.xml index 3f8f7fc..3799e81 100644 --- a/blade-auth/pom.xml +++ b/blade-auth/pom.xml @@ -63,6 +63,10 @@ org.springblade blade-system-api + + org.springblade + blade-mk-api + org.springblade blade-resource-api diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2MKEndpoint.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2MKEndpoint.java new file mode 100644 index 0000000..b38eff2 --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2MKEndpoint.java @@ -0,0 +1,68 @@ +package org.springblade.auth.endpoint; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.oauth2.endpoint.AbstractOAuth2MKEndpoint; +import org.springblade.core.oauth2.granter.TokenGranterFactory; +import org.springblade.core.oauth2.handler.TokenHandler; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.UrlUtil; +import org.springblade.thirdparty.mk.config.MKProperties; +import org.springblade.thirdparty.mk.service.IMKService; +import org.springframework.web.bind.annotation.RestController; + +import java.util.stream.Stream; + +/** + * @author bfhuange + * @date 2024/9/17 + */ +@RestController +@Slf4j +@Tag(name = "跳转mk认证", description = "跳转mk认证端点") +public class OAuth2MKEndpoint extends AbstractOAuth2MKEndpoint { + /** + * 登录链接 + */ + private static final String LOGIN_URL_FORMAT = "%s%s?appId=%s&redirection_url=%s"; + + private final MKProperties mkProperties; + private final IMKService mkService; + + public OAuth2MKEndpoint(TokenGranterFactory granterFactory, TokenHandler tokenHandler, MKProperties mkProperties, IMKService mkService) { + super(granterFactory, tokenHandler); + this.mkProperties = mkProperties; + this.mkService = mkService; + } + + @Override + protected String generateLoginUrl(String refererUrl) { + // 重定向到来源 + return String.format(LOGIN_URL_FORMAT, mkProperties.getLoginUrl(), mkProperties.getMkSsoLoginUrl(), mkProperties.getOauthAppId(), UrlUtil.encode(refererUrl)); + } + + @Override + protected String getAccountByMkCode(String mkCode) { + return mkService.getMKAccount(mkCode); + } + + @Override + protected boolean checkRefererUrl(String refererUrl) { + if (!mkProperties.isCheckReferer()) { + // 未开启校验来源,校验通过 + return true; + } + if (StringUtil.isBlank(mkProperties.getErpBaseUrls())) { + log.error("未配置erp地址,无法校验来源"); + return false; + } + if (StringUtil.isBlank(refererUrl)) { + // 来源地址为空,通过,以便用于调试 + return true; + } + // 匹配到一个erp基础地址就行了 + return Stream.of(mkProperties.getErpBaseUrls().split(",")) + .map(String::trim) + .anyMatch(refererUrl::startsWith); + } +} diff --git a/blade-auth/src/main/java/org/springblade/auth/exception/OAuth2MKExceptionHandler.java b/blade-auth/src/main/java/org/springblade/auth/exception/OAuth2MKExceptionHandler.java new file mode 100644 index 0000000..7ee4962 --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/exception/OAuth2MKExceptionHandler.java @@ -0,0 +1,13 @@ +package org.springblade.auth.exception; + +import org.springblade.auth.endpoint.OAuth2MKEndpoint; +import org.springblade.core.oauth2.exception.OAuth2ExceptionHandler; +import org.springframework.web.bind.annotation.ControllerAdvice; + +/** + * @author bfhuange + * @date 2024/10/16 + */ +@ControllerAdvice(basePackageClasses = OAuth2MKEndpoint.class) +public class OAuth2MKExceptionHandler extends OAuth2ExceptionHandler { +} diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index d81e0c6..c677b13 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -8,6 +8,7 @@ spring: import: - nacos:blade.yaml?group=DEFAULT_GROUP&refreshEnabled=true - nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:third-party-api.yaml?group=DEFAULT_GROUP&refreshEnabled=true - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true cloud: nacos: From 0d247afef325a59669adaf4466925b7313b644ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Sun, 9 Aug 2026 22:15:15 +0800 Subject: [PATCH 05/12] =?UTF-8?q?feat(process):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=B5=81=E7=A8=8B=E7=AE=A1=E7=90=86=E7=9B=B8?= =?UTF-8?q?=E5=85=B3API=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-service-api/blade-process-api/pom.xml | 16 + .../process/feign/IBusinessProcessClient.java | 105 +++ .../dto/AdditionOperationParameterDTO.java | 34 + .../process/pojo/dto/ApprovalDTO.java | 119 +++ ...sinessProcessCurrentHandlerRefreshDTO.java | 41 + .../pojo/dto/BusinessProcessDeleteDTO.java | 39 + .../pojo/dto/BusinessProcessQueryDTO.java | 42 + .../pojo/dto/BusinessProcessSubmitDTO.java | 81 ++ .../pojo/dto/BusinessProcessUpdateDTO.java | 39 + .../process/pojo/dto/ProcessExecuteDTO.java | 74 ++ .../process/CommonDeptCodeProcessParam.java | 22 + .../process/pojo/entity/BusinessProcess.java | 164 ++++ .../process/pojo/enums/ApproveStatusEnum.java | 165 ++++ .../process/pojo/enums/TodoStatus.java | 36 + .../process/pojo/vo/ApprovalVO.java | 177 +++++ .../pojo/vo/BusinessProcessListVO.java | 110 +++ .../process/pojo/vo/BusinessProcessVO.java | 43 ++ .../pojo/vo/ProcessApprovedRecordVO.java | 110 +++ .../process/pojo/vo/ProcessAttachmentVO.java | 43 ++ .../process/pojo/vo/ProcessCommentVO.java | 63 ++ .../process/pojo/vo/ProcessTodoVO.java | 79 ++ blade-service-api/pom.xml | 1 + blade-service/blade-system/pom.xml | 4 + .../controller/BusinessProcessController.java | 83 ++ .../process/convert/ApprovalConvert.java | 51 ++ .../convert/BusinessProcessConvert.java | 84 ++ .../process/convert/MKApprovalConvert.java | 149 ++++ .../process/feign/BusinessProcessClient.java | 77 ++ .../process/mapper/BusinessProcessMapper.java | 14 + .../process/mapper/BusinessProcessMapper.xml | 29 + .../service/IBusinessProcessService.java | 115 +++ .../impl/BusinessProcessServiceImpl.java | 725 ++++++++++++++++++ .../springblade/system/SystemApplication.java | 2 + pom.xml | 5 + 34 files changed, 2941 insertions(+) create mode 100644 blade-service-api/blade-process-api/pom.xml create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/AdditionOperationParameterDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ApprovalDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessCurrentHandlerRefreshDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessDeleteDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessQueryDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessSubmitDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessUpdateDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/process/CommonDeptCodeProcessParam.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/ApproveStatusEnum.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/TodoStatus.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ApprovalVO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessListVO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessVO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessApprovedRecordVO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessAttachmentVO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessCommentVO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessTodoVO.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/convert/ApprovalConvert.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/convert/BusinessProcessConvert.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/convert/MKApprovalConvert.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java diff --git a/blade-service-api/blade-process-api/pom.xml b/blade-service-api/blade-process-api/pom.xml new file mode 100644 index 0000000..b4d7371 --- /dev/null +++ b/blade-service-api/blade-process-api/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + org.springblade + blade-service-api + ${revision} + + + blade-process-api + ${project.artifactId} + jar + + diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java new file mode 100644 index 0000000..9218fe8 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java @@ -0,0 +1,105 @@ +package org.springblade.process.feign; + +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.tool.api.FR; +import org.springblade.process.pojo.dto.BusinessProcessCurrentHandlerRefreshDTO; +import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO; +import org.springblade.process.pojo.dto.BusinessProcessSubmitDTO; +import org.springblade.process.pojo.dto.BusinessProcessUpdateDTO; +import org.springblade.process.pojo.vo.BusinessProcessVO; +import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; +import org.springblade.process.pojo.vo.ProcessTodoVO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * 业务流程关联表 Feign接口类 + * + * @author BladeX + * @since 2024-09-19 + */ +@FeignClient( + value = AppConstant.APPLICATION_SYSTEM_NAME +) +public interface IBusinessProcessClient { + + String API_PREFIX = "/feign/client/businessProcess"; + String SUBMIT_BUSINESS_PROCESS = API_PREFIX + "/submitBusinessProcess"; + String UPDATE_BUSINESS_PROCESS_APPROVER = API_PREFIX + "/updateBusinessProcessApprover"; + String REFRESH_BUSINESS_PROCESS_CURRENT_HANDLERS = API_PREFIX + "/refreshBusinessProcessCurrentHandlers"; + String UPDATE_BUSINESS_PROCESS_STATUS = API_PREFIX + "/updateBusinessProcessStatus"; + String DELETE_BUSINESS_PROCESS = API_PREFIX + "/deleteBusinessProcess"; + String QUERY_TODO_LIST = API_PREFIX + "/queryTodoList"; + String QUERY_BUSINESS_PROCESS_SNAPSHOT = API_PREFIX + "/queryBusinessProcessSnapshot"; + String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments"; + + /** + * 提交业务流程 + * @param param + */ + @PostMapping(SUBMIT_BUSINESS_PROCESS) + FR submitBusinessProcess(@Validated @RequestBody BusinessProcessSubmitDTO param); + + /** + * 修改业务流程状态 + * @param param + * @return + */ + @PostMapping(UPDATE_BUSINESS_PROCESS_STATUS) + FR updateBusinessProcessStatus(@Validated @RequestBody BusinessProcessUpdateDTO param); + + /** + * 修改业务流程审批人 + * @param param + * @return + */ + @PostMapping(UPDATE_BUSINESS_PROCESS_APPROVER) + FR updateBusinessProcessApprover(@Validated @RequestBody BusinessProcessUpdateDTO param); + + /** + * 只刷新当前节点和当前处理人 + * @param param + * @return + */ + @PostMapping(REFRESH_BUSINESS_PROCESS_CURRENT_HANDLERS) + FR refreshBusinessProcessCurrentHandlers(@Validated @RequestBody BusinessProcessCurrentHandlerRefreshDTO param); + + /** + * 查询流程当前待办列表 + * @param processInstanceId + * @return + */ + @GetMapping(QUERY_TODO_LIST) + FR> queryTodoList(@RequestParam("processInstanceId") String processInstanceId); + + /** + * 查询业务流程当前快照 + * @param processInstanceId + * @return + */ + @GetMapping(QUERY_BUSINESS_PROCESS_SNAPSHOT) + FR queryBusinessProcessSnapshot(@RequestParam("processInstanceId") String processInstanceId); + + /** + * 删除业务流程 + * @param param + * @return + */ + @PostMapping(DELETE_BUSINESS_PROCESS) + FR deleteBusinessProcess(@Validated @RequestBody BusinessProcessDeleteDTO param); + + /** + * 查询流程审批记录不处理附件 + * @param bizId + * @param processInstanceId + * @return + */ + @GetMapping(QUERY_APPROVED_RECORD_LIST) + FR> queryApprovedRecordsNoAttachments(@RequestParam(name = "bizId", required = false) String bizId, @RequestParam(name = "processInstanceId", required = false) String processInstanceId); +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/AdditionOperationParameterDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/AdditionOperationParameterDTO.java new file mode 100644 index 0000000..211613e --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/AdditionOperationParameterDTO.java @@ -0,0 +1,34 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 附加操作信息 + * + * @author linbb + */ +@Schema(description = "附加操作信息") +@Data +public class AdditionOperationParameterDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 操作类型 + */ + private String operationType; + + /** + * 操作身份 + */ + private String operationIdentity; + + /** + * 操作参数 + */ + private String parameter; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ApprovalDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ApprovalDTO.java new file mode 100644 index 0000000..d2a2055 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ApprovalDTO.java @@ -0,0 +1,119 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * mk审批中心查询参数 + * @author bfhuange + * @since 2025/4/2 + */ +@Data +@Schema(description = "mk审批中心查询参数") +public class ApprovalDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 单据类型 myApproving 我的待审,myApproved 我的已审,myReading 我的待阅,myReaded 我的已阅,myRelated 我参与的,myCreated 我发起的 + */ + @NotBlank(message = "单据类型不能为空") + @Schema(description = "单据类型 myApproving 我的待审,myApproved 我的已审,myReading 我的待阅,myReaded 我的已阅,myRelated 我参与的,myCreated 我发起的") + private String docType; + /** + * 关键字 + */ + @Schema(description = "关键字") + private String keyword; + /** + * 模板名称 + */ + @Schema(description = "模板名称") + private String templateName; + /** + * 申请时间开始 + */ + @Schema(description = "申请时间开始") + private Date applicantTimeStart; + /** + * 申请时间结束 + */ + @Schema(description = "申请时间结束") + private Date applicantTimeEnd; + + //==================================待办参数======================= + + /** + * 接收时间开始 + */ + @Schema(description = "接收时间开始") + private Date receiveTimeStart; + /** + * 接收时间结束 + */ + @Schema(description = "接收时间结束") + private Date receiveTimeEnd; + + //==================================已处理参数======================= + + /** + * 流程状态 00 – 废弃、10 – 草稿、11 – 驳回 20 – 待审、21 – 异常、40 – 挂起 30 – 结束 + */ + @Schema(description = "流程状态 00 – 废弃、10 – 草稿、11 – 驳回 20 – 待审、21 – 异常、40 – 挂起 30 – 结束") + private String status; + + /** + * 结束时间开始 + */ + @Schema(description = "结束时间开始") + private Date finishTimeStart; + /** + * 结束时间结束 + */ + @Schema(description = "结束时间结束") + private Date finishTimeEnd; + + /** + * 最后处理时间开始 + */ + @Schema(description = "最后处理时间开始") + private Date lastHandleStart; + /** + * 最后处理时间结束 + */ + @Schema(description = "最后处理时间结束") + private Date lastHandleEnd; + + //==================================已阅参数======================= + /** + * 阅读时间开始 + */ + @Schema(description = "阅读时间开始") + private Date readTimeStart; + /** + * 阅读时间结束 + */ + @Schema(description = "阅读时间结束") + private Date readTimeEnd; + + //==================================我参与的参数======================= + /** + * 创建时间开始 + */ + @Schema(description = "创建时间开始") + private Date createTimeStart; + /** + * 创建时间结束 + */ + @Schema(description = "创建时间结束") + private Date createTimeEnd; + /** + * 登录名 + */ + @Schema(description = "登录名", hidden = true) + private String loginName; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessCurrentHandlerRefreshDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessCurrentHandlerRefreshDTO.java new file mode 100644 index 0000000..b56c7af --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessCurrentHandlerRefreshDTO.java @@ -0,0 +1,41 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 业务流程当前处理人刷新参数 + * + * @author bfhuange + * @date 2026/4/9 + */ +@Schema(description = "业务流程当前处理人刷新参数") +@Data +public class BusinessProcessCurrentHandlerRefreshDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 流程实例id + */ + @NotBlank(message = "流程实例id不能为空") + @Schema(description = "流程实例id") + private String processInstanceId; + + /** + * 发起人登录名,可为空。 + * 为空时优先从业务流程表回填;仅当业务流程不存在时,才回退使用调用方传入值。 + */ + @Schema(description = "发起人登录名,可为空;为空时优先从业务流程表回填") + private String promoterLoginName; + + /** + * 是否流程已完成 + */ + @Schema(description = "是否流程已完成") + private boolean complete; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessDeleteDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessDeleteDTO.java new file mode 100644 index 0000000..3e78657 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessDeleteDTO.java @@ -0,0 +1,39 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 业务流程删除参数 + * + * @author BladeX + * @since 2024-11-26 + */ +@NoArgsConstructor +@Schema(description = "业务流程删除参数") +@Data +public class BusinessProcessDeleteDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 业务id + */ + @NotNull(message = "业务id不能为空") + @Schema(description = "业务id") + private Long bizId; + /** + * 发起人登录名,即erp手机号或账号 + */ + // @NotBlank(message = "发起人登录名不能为空") + @Schema(description = "发起人登录名,即erp手机号或账号") + private String promoterLoginName; + + public BusinessProcessDeleteDTO(Long bizId) { + this.bizId = bizId; + } +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessQueryDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessQueryDTO.java new file mode 100644 index 0000000..ff5c35b --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessQueryDTO.java @@ -0,0 +1,42 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 业务流程查询参数 + * + * @author BladeX + * @since 2024-09-23 + */ +@Schema(description = "业务流程查询参数") +@Data +public class BusinessProcessQueryDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 流程类型 + */ + @Schema(description = "审批类型") + private String processType; + /** + * 文档编号 + */ + @Schema(description = "审批编号") + private String docCode; + /** + * 类型 todo:待审批,create:我创建的,done:我参与的 + */ + @NotBlank(message = "类型不能为空") + @Schema(defaultValue = "类型 todo:待审批,create:我创建的,done:我参与的") + private String type; + /** + * 当前登录人账号 + */ + @Schema(hidden = true) + private String loginName; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessSubmitDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessSubmitDTO.java new file mode 100644 index 0000000..d6a244d --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessSubmitDTO.java @@ -0,0 +1,81 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 业务流程提交参数 + * + * @author BladeX + * @since 2024-09-19 + */ +@Schema(description = "业务流程提交参数") +@Data +public class BusinessProcessSubmitDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 业务id + */ + @NotNull(message = "业务id不能为空") + @Schema(description = "业务id") + private Long bizId; + /** + * 流程类型 + */ + @NotBlank(message = "流程类型不能为空") + @Schema(description = "流程类型") + private String processType; + /** + * 文档编号 + */ + @Schema(description = "文档编号") + private String docCode; + /** + * 标题 + */ + @Schema(description = "标题") + private String subject; + /** + * 发起人id,空自动取登录人id + */ + @Schema(description = "发起人id") + private Long promoterId; + /** + * 发起人名称,空自动取登录人名称 + */ + @Schema(description = "发起人名称") + private String promoterName; + /** + * 发起人登录名 + */ + @Schema(description = "发起人登录名") + private String promoterLoginName; + /** + * 提交时间,空自动取当前时间 + */ + @Schema(description = "提交时间") + private Date submitTime; + /** + * 流程参数,如果流程没有用到参数做条件判断或动态部门,可以不传 + */ + @Schema(description = "流程参数") + private T processParam; + /** + * 流程执行参数,透传mk参数 + */ + @Schema(description = "流程执行参数") + private ProcessExecuteDTO executeParam; + /** + * 添加群组编码 + */ + @Schema(description = "添加群组编码") + private boolean addGroupCode; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessUpdateDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessUpdateDTO.java new file mode 100644 index 0000000..865174b --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/BusinessProcessUpdateDTO.java @@ -0,0 +1,39 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; + +/** + * 业务流程修改参数 + * + * @author BladeX + * @since 2024-09-19 + */ +@Schema(description = "业务流程修改参数") +@Data +public class BusinessProcessUpdateDTO extends BusinessProcessCurrentHandlerRefreshDTO { + @Serial + private static final long serialVersionUID = 1L; + /** + * 操作节点id,流程审批结束为空 + */ + @Schema(description = "操作节点id") + private String operationNodeId; + /** + * 操作节点编号,流程审批结束为空 + */ + @Schema(description = "操作节点编号") + private String operationNodeNumber; + /** + * 驳回节点id N2 是起草节点 + */ + @Schema(description = "驳回节点id") + private String rejectNodeId; + /** + * 审批状态 + */ + @Schema(description = "审批状态") + private String approveStatus; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java new file mode 100644 index 0000000..981167c --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java @@ -0,0 +1,74 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.system.pojo.dto.AdditionOperationParameterDTO; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * mk 流程执行参数 + * @author bfhuange + * @date 2024/9/26 + */ +@Schema(description = "mk 流程执行参数") +@Data +public class ProcessExecuteDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 表单实例id,业务id + */ + private String formInstanceId; + /** + * 登录账号 登录用户账号/手机号 + */ + private String loginName; + /** + * 流程标题 + */ + private String subject; + /** + * 任务ID + */ + private String taskId; + /** + * 任务类型 + */ + private String activityType; + /** + * 操作详细参数(json) + */ + private String parameter; + /** + * 流程实例ID + */ + private String processId; + /** + * 操作标识(相同操作类型和相同操作身份可能存在多个操作配置) + */ + private String operationId; + /** + * 操作类型 + */ + private String operationType; + /** + * 操作身份 + */ + private String operationIdentity; + /** + * 附加操作参数信息 + */ + private List additionParameters; + /** + * 表单实例Model Name + */ + private String formInstanceModel; + /** + * 业务表单字段值集合 + */ + // private Map formValues; + private Object formValues; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/process/CommonDeptCodeProcessParam.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/process/CommonDeptCodeProcessParam.java new file mode 100644 index 0000000..8044194 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/process/CommonDeptCodeProcessParam.java @@ -0,0 +1,22 @@ +package org.springblade.process.pojo.dto.process; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 公共部门编码流程参数 + * @author bfhuange + * @date 2024/10/9 + */ +@Data +public class CommonDeptCodeProcessParam implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 部门编码 + */ + private String deptCode; + +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java new file mode 100644 index 0000000..bac2329 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java @@ -0,0 +1,164 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.process.pojo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 业务流程关联表 实体类 + * + * @author BladeX + * @since 2024-09-19 + */ +@Data +@TableName("blade_business_process") +@Schema(description = "BusinessProcess对象") +public class BusinessProcess implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "主键") + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + + /** + * 业务id + */ + @Schema(description = "业务id") + private Long bizId; + /** + * 流程实例id + */ + @Schema(description = "流程实例id") + private String processInstanceId; + /** + * 流程类型 + */ + @Schema(description = "流程类型") + private String processType; + /** + * 文档编号 + */ + @Schema(description = "文档编号") + private String docCode; + /** + * 标题 + */ + @Schema(description = "标题") + private String subject; + /** + * 发起人id + */ + @Schema(description = "发起人id") + private Long promoterId; + /** + * 发起人名称 + */ + @Schema(description = "发起人名称") + private String promoterName; + /** + * 发起人登录名 + */ + @Schema(description = "发起人登录名") + private String promoterLoginName; + /** + * 提交时间 + */ + @Schema(description = "提交时间") + private Date submitTime; + /** + * 完成时间 + */ + @Schema(description = "完成时间") + private Date completeTime; + /** + * 当前节点id,多个用逗号拼接 + */ + @Schema(description = "当前节点id,多个用逗号拼接") + private String currentNodeIds; + /** + * 当前节点名称,多个用逗号拼接 + */ + @Schema(description = "当前节点名称,多个用逗号拼接") + private String currentNodeNames; + /** + * 当前处理人,多个用逗号拼接 + */ + @Schema(description = "当前处理人,多个用逗号拼接") + private String currentHandlers; + /** + * 接收时间 + */ + @Schema(description = "接收时间") + private Date receiveTime; + /** + * 是否已完成(0:未完成, 1:已完成) + */ + @Schema(description = "是否已完成") + private Integer isCompleted; + /** + * 审批状态 + */ + @Schema(description = "审批状态") + private String approveStatus; + /** + * 租户ID + */ + @Schema(description = "租户ID") + private String tenantId; + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATE) + @Schema(description = "创建时间", hidden = true) + @TableField(fill = FieldFill.INSERT) + private Date createTime; + /** + * 更新时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATE) + @Schema(description = "更新时间", hidden = true) + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/ApproveStatusEnum.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/ApproveStatusEnum.java new file mode 100644 index 0000000..9b9aafe --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/ApproveStatusEnum.java @@ -0,0 +1,165 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.process.pojo.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.Objects; + +/** + * 审批状态 + * + * @author LiuXinjie + * @apiNote 合同审批状态 + */ +@Getter +@AllArgsConstructor +public enum ApproveStatusEnum { + + /** + * 默认编号 + */ + DRAFT("draft", "草稿"), //可提交 + APPROVING("approval", "审批中"), + APPROVED("pass", "审批通过"), + REJECTED("reject", "审批驳回"), //通用的流程 驳回可编辑 + REVOCATION("revocation", "已撤回"), //可重新提交 + ABANDON("abandon", "废弃"), + ; + + final String value; + final String text; + + public boolean match(String value){ + return this.value.equals(value); + } + + public static String getValueByText(String text) { + if (text == null) { + return null; + } + for (ApproveStatusEnum item : values()) { + if (Objects.equals(item.getText(), text)) { + return item.getValue(); + } + } + return null; + } + + public static String getTextByValue(String value) { + if (value == null) { + return null; + } + for (ApproveStatusEnum item : values()) { + if (Objects.equals(item.getValue(), value)) { + return item.getText(); + } + } + return null; + } + + /** + * 是否可以撤回 + * + * @param value + * @return + */ + public static boolean canRevoke(String value) { + return APPROVING.getValue().equals(value); + } + + /** + * 能不能删除审批流 + * @param value + * @return + */ + public static boolean canDelAuditFlow(String value){ + return REJECTED.getValue().equals(value) || REVOCATION.getValue().equals(value); + } + + /** + * 驳回或撤回 + * @return + */ + public static boolean rejectedOrRevocation(String approveStatus) { + return canDelAuditFlow(approveStatus); + } + + /** + * 能不能删除数据 + * @param value + * @return + */ + public static boolean canDeleteData(String value){ + return ABANDON.getValue().equals(value) || DRAFT.getValue().equals(value) || canDelAuditFlow(value); + } + + public static String getNameStr(String value) { + for (ApproveStatusEnum state : values()) { + if (state.value.equals(value)) { + return state.text; + } + } + return null; + } + + + /** + * 是否可以编辑表单 + * + * @param value + * @return + */ + public static boolean canEdit(String value) { + return DRAFT.getValue().equals(value) || REJECTED.getValue().equals(value) || REVOCATION.getValue().equals(value); + } + + /** + * 获取可驳回状态 + * + * @return + */ + public static List buildPreviousApproveStatusList(String currentStatus) { + if (ApproveStatusEnum.APPROVING.getValue().equals(currentStatus)) { + // 变更成审批中,前置条件为 草稿或者审批中 + return List.of(ApproveStatusEnum.APPROVING.getValue()); + } else if (ApproveStatusEnum.APPROVED.getValue().equals(currentStatus)) { + // 变更成审批通过,前置条件为 审批中 + return List.of(ApproveStatusEnum.APPROVING.getValue()); + } else if (ApproveStatusEnum.REJECTED.getValue().equals(currentStatus)) { + // 变更成审批驳回,前置条件为 审批中 + return List.of(ApproveStatusEnum.APPROVING.getValue()); + } else if (ApproveStatusEnum.REVOCATION.getValue().equals(currentStatus)) { + // 变更成撤回,前置条件为 审批中 + return List.of(ApproveStatusEnum.APPROVING.getValue()); + } else { + // 其他情况,前置条件为 草稿 + return List.of(ApproveStatusEnum.DRAFT.getValue()); + } + } +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/TodoStatus.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/TodoStatus.java new file mode 100644 index 0000000..962399e --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/enums/TodoStatus.java @@ -0,0 +1,36 @@ +package org.springblade.process.pojo.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 待办状态枚举 + * @author bfhuange + * @date 2024/9/20 + */ +@AllArgsConstructor +@Getter +public enum TodoStatus { + /** + * 待办 + */ + TODO("todo", "待办"), + /** + * 已办 + */ + DONE("done", "已办"), + /** + * 身份重复跳过 + */ + SKIP("skip", "身份重复跳过"), + ; + + /** + * 待办状态编码 + */ + private final String code; + /** + * 待办状态名称 + */ + private final String name; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ApprovalVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ApprovalVO.java new file mode 100644 index 0000000..5c05c88 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ApprovalVO.java @@ -0,0 +1,177 @@ +package org.springblade.process.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * @author bfhuange + * @since 2025/4/2 + */ +@Data +@Schema(description = "mk审批中心记录") +public class ApprovalVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * id + */ + @Schema(description = "id") + private String id; + /** + * 流程所属应用 + */ + @Schema(description = "流程所属应用") + private String appName; + /** + * 申请人名称 + */ + @Schema(description = "申请人名称") + private String applicantName; + /** + * 申请人登录名 + */ + @Schema(description = "申请人登录名") + private String applicantLoginName; + /** + * 发起人名称 + */ + @Schema(description = "发起人名称") + private String creator; + /** + * 处理人名称 + */ + @Schema(description = "处理人名称") + private String handlerName; + /** + * 处理人登录名 + */ + @Schema(description = "处理人登录名") + private String handlerLoginName; + /** + * 当前处理人名称 + */ + @Schema(description = "当前处理人名称") + private String currentHandler; + /** + * 节点名称 + */ + @Schema(description = "节点名称") + private String nodeName; + /** + * 流程id + */ + @Schema(description = "流程id") + private String processId; + /** + * 流程发起时间 + */ + @Schema(description = "流程发起时间") + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME, timezone = "GMT+8") + private Date startTime; + /** + * 创建时间 + */ + @Schema(description = "创建时间") + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME, timezone = "GMT+8") + private Date createTime; + /** + * 如果是待办:待办接收时间 如果是待阅:传阅接收时间 + */ + @Schema(description = "如果是待办:待办接收时间 如果是待阅:传阅接收时间") + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME, timezone = "GMT+8") + private Date receiveTime; + /** + * 任务结束时间 + */ + @Schema(description = "任务结束时间") + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME, timezone = "GMT+8") + private Date finishTime; + /** + * 阅读时间(待阅任务) + */ + @Schema(description = "阅读时间(待阅任务)") + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME, timezone = "GMT+8") + private Date readTime; + /** + * 最后处理时间(待审任务) + */ + @Schema(description = "最后处理时间(待审任务)") + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME, timezone = "GMT+8") + private Date lastHandleTime; + /** + * 流程结束时间 + */ + @Schema(description = "流程结束时间") + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME, timezone = "GMT+8") + private Date processFinishTime; + /** + * 流程状态 00 – 废弃、10 – 草稿、11 – 驳回 20 – 待审、21 – 异常、40 – 挂起 30 – 结束 + */ + @Schema(description = "流程状态 00 – 废弃、10 – 草稿、11 – 驳回 20 – 待审、21 – 异常、40 – 挂起 30 – 结束") + private String status; + /** + * 流程状态名称 + */ + @Schema(description = "流程状态名称") + private String statusStr; + /** + * 流程主题 + */ + @Schema(description = "流程主题") + private String subject; + /** + * 模板编码 + */ + @Schema(description = "模板编码") + private String templateCode; + /** + * 任务id + */ + @Schema(description = "任务id") + private String taskId; + /** + * 任务状态 20 - 激活、30 - 结束、40 - 挂起、50 - 自动跳过 + */ + @Schema(description = "任务状态 20 - 激活、30 - 结束、40 - 挂起、50 - 自动跳过") + private String taskStatus; + /** + * 任务标题 + */ + @Schema(description = "任务标题") + private String taskSubject; + /** + * 催办标记 + */ + @Schema(description = "催办标记") + private String urgeTab; + /** + * 任务类型 1待办,2待阅 + */ + @Schema(description = "任务类型 1待办,2待阅") + private Integer taskType; + /** + * 待办优先级 + */ + @Schema(description = "待办优先级") + private Integer level; + /** + * 模板名称中文 + */ + @Schema(description = "模板名称中文") + private String templateNameCn; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessListVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessListVO.java new file mode 100644 index 0000000..0d1d1b6 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessListVO.java @@ -0,0 +1,110 @@ +package org.springblade.process.pojo.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * @author bfhuange + * @date 2024/9/23 + */ +@Data +@Schema(description = "BusinessProcessListVO") +public class BusinessProcessListVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "主键") + private Long id; + + /** + * 业务id + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "业务id") + private Long bizId; + /** + * 流程实例id + */ + @Schema(description = "流程实例id") + private String processInstanceId; + /** + * 流程类型 + */ + @Schema(description = "审批类型") + private String processType; + /** + * 流程类型名称 + */ + @Schema(description = "审批类型名称") + private String processTypeStr; + /** + * 文档编号 + */ + @Schema(description = "审批单号") + private String docCode; + /** + * 发起人id + */ + @Schema(description = "发起人id") + private Long promoterId; + /** + * 发起人名称 + */ + @Schema(description = "发起人名称") + private String promoterName; + /** + * 提交时间 + */ + @Schema(description = "提交时间") + private Date submitTime; + /** + * 接收时间 + */ + @Schema(description = "接收时间") + private Date receiveTime; + /** + * 完成时间 + */ + @Schema(description = "完成时间") + private Date completeTime; + /** + * 当前节点id,多个用逗号拼接 + */ + @Schema(description = "当前节点id,多个用逗号拼接") + private String currentNodeIds; + /** + * 当前节点名称,多个用逗号拼接 + */ + @Schema(description = "当前节点名称,多个用逗号拼接") + private String currentNodeNames; + /** + * 当前处理人,多个用逗号拼接 + */ + @Schema(description = "当前处理人,多个用逗号拼接") + private String currentHandlers; + /** + * 是否已完成(0:未完成, 1:已完成) + */ + @Schema(description = "是否已完成(0:未完成, 1:已完成)") + private Integer isCompleted; + /** + * 审批状态 + */ + @Schema(description = "审批状态") + private String approveStatus; + /** + * 审批状态名称 + */ + @Schema(description = "审批状态名称") + private String approveStatusStr; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessVO.java new file mode 100644 index 0000000..67b22eb --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/BusinessProcessVO.java @@ -0,0 +1,43 @@ +package org.springblade.process.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * @author bfhuange + * @date 2024/9/20 + */ +@Data +@Schema(description = "BusinessProcessVO") +public class BusinessProcessVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 流程实例id + */ + private String processInstanceId; + /** + * 当前节点id,多个用逗号拼接 + */ + @Schema(description = "当前节点id,多个用逗号拼接") + private String currentNodeIds; + /** + * 当前节点名称,多个用逗号拼接 + */ + @Schema(description = "当前节点名称,多个用逗号拼接") + private String currentNodeNames; + /** + * 当前处理人,多个用逗号拼接 + */ + @Schema(description = "当前处理人,多个用逗号拼接") + private String currentHandlers; + /** + * 接收时间 + */ + @Schema(description = "接收时间") + private Date receiveTime; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessApprovedRecordVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessApprovedRecordVO.java new file mode 100644 index 0000000..e6a96d6 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessApprovedRecordVO.java @@ -0,0 +1,110 @@ +package org.springblade.process.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * @author bfhuange + * @since 2025/2/24 + */ +@Data +@Schema(description = "流程审批记录") +public class ProcessApprovedRecordVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * 记录主键 + */ + @Schema(description = "id") + private String id; + /** + * 处理人 + */ + @Schema(description = "处理人") + private String handler; + /** + * 操作 + */ + @Schema(description = "操作") + private String action; + /** + * 操作编码 + */ + @Schema(description = "操作编码") + private String actionCode; + /** + * 操作描述(系统操作) + */ + @Schema(description = "操作描述(系统操作)") + private String actionDesc; + /** + * 操作名称 + */ + @Schema(description = "操作名称") + private String actionName; + /** + * 处理意见 + */ + @Schema(description = "处理意见") + private String message; + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "创建时间") + private Date createTime; + /** + * 流程实例id + */ + @Schema(description = "流程实例id") + private String processInstanceId; + /** + * 节点实例id + */ + @Schema(description = "节点实例id") + private String nodeInstanceId; + /** + * 节点类型 + */ + @Schema(description = "节点类型") + private String nodeType; + /** + * 节点id + */ + @Schema(description = "节点id") + private String nodeId; + /** + * 节点名称 + */ + @Schema(description = "节点名称") + private String nodeName; + /** + * 节点编号 + */ + @Schema(description = "节点编号") + private String nodeNumber; + /** + * 抄送人列表 + */ + @Schema(description = "抄送人列表") + private List senders; + /** + * 附件参数 + */ + @Schema(description = "附件参数") + private List attachmentParameter; + /** + * 流程附言 + */ + @Schema(description = "流程附言") + private List processComments; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessAttachmentVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessAttachmentVO.java new file mode 100644 index 0000000..4e0548e --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessAttachmentVO.java @@ -0,0 +1,43 @@ +package org.springblade.process.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author bfhuange + * @since 2025/2/24 + */ +@Data +@Schema(description = "流程附件") +public class ProcessAttachmentVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * id + */ + @Schema(description = "id") + private String id; + /** + * 附件名称 + */ + @Schema(description = "附件名称") + private String name; + /** + * 附件类型 electronicSign 电子签名,attachment 附件 + */ + @Schema(description = "附件类型 electronicSign 电子签名,attachment 附件") + private String type; + /** + * 附件ID + */ + @Schema(description = "附件ID") + private String fileId; + /** + * base64 + */ + @Schema(description = "base64") + private String base64; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessCommentVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessCommentVO.java new file mode 100644 index 0000000..b95f96d --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessCommentVO.java @@ -0,0 +1,63 @@ +package org.springblade.process.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 流程附言 + * @author bfhuange + * @since 2025/2/24 + */ +@Data +@Schema(description = "流程附言") +public class ProcessCommentVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + /** + * id + */ + @Schema(description = "id") + private String id; + /** + * 附言内容 + */ + @Schema(description = "附言内容") + private String content; + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "创建时间") + private Date createTime; + /** + * 更新时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "更新时间") + private Date updateTime; + /** + * 用户id + */ + @Schema(description = "用户id") + private String userId; + /** + * 用户名称 + */ + @Schema(description = "用户名称") + private String userName; + /** + * 附言附件 + */ + @Schema(description = "附言附件") + private List attachments; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessTodoVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessTodoVO.java new file mode 100644 index 0000000..8b4b615 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/ProcessTodoVO.java @@ -0,0 +1,79 @@ +package org.springblade.process.pojo.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * @author bfhuange + * @since 2025/3/10 + */ +@Data +@Schema(description = "流程待办") +public class ProcessTodoVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "主键") + private Long id; + + /** + * 流程实例id + */ + @Schema(description = "流程实例id") + private String processInstanceId; + /** + * 节点id + */ + @Schema(description = "节点id") + private String nodeId; + /** + * 节点编号 + */ + @Schema(description = "节点编号") + private String nodeNumber; + /** + * 节点名称 + */ + @Schema(description = "节点名称") + private String nodeName; + /** + * mk登录名 + */ + @Schema(description = "mk登录名") + private String loginName; + /** + * 用户姓名 + */ + @Schema(description = "用户姓名") + private String userName; + /** + * 状态(todo:待办,done:已办,skip:身份重复跳过) + */ + @Schema(description = "状态(todo:待办,done:已办,skip:身份重复跳过)") + private String status; + /** + * 接收时间 + */ + @Schema(description = "接收时间") + private Date receiveTime; + /** + * 操作时间 + */ + @Schema(description = "操作时间") + private Date operationTime; + /** + * 操作名称 + */ + @Schema(description = "操作名称") + private String operationName; +} diff --git a/blade-service-api/pom.xml b/blade-service-api/pom.xml index 0cca155..538df59 100644 --- a/blade-service-api/pom.xml +++ b/blade-service-api/pom.xml @@ -25,6 +25,7 @@ blade-record-api blade-file-api blade-open-api + blade-process-api diff --git a/blade-service/blade-system/pom.xml b/blade-service/blade-system/pom.xml index 4ab8af9..d3bde19 100644 --- a/blade-service/blade-system/pom.xml +++ b/blade-service/blade-system/pom.xml @@ -45,6 +45,10 @@ org.springblade blade-user-api + + org.springblade + blade-process-api + org.springblade diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java b/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java new file mode 100644 index 0000000..c976569 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java @@ -0,0 +1,83 @@ +package org.springblade.process.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; +import org.springblade.process.pojo.dto.ApprovalDTO; +import org.springblade.process.pojo.vo.ApprovalVO; +import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; +import org.springblade.process.service.IBusinessProcessService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 业务流程关联表 控制器 + * + * @author BladeX + * @since 2024-09-19 + */ +@Valid +@RestController +@AllArgsConstructor +@RequestMapping("businessProcess") +@Tag(name = "业务流程关联表", description = "业务流程关联表接口") +public class BusinessProcessController extends BladeController { + + private final IBusinessProcessService businessProcessService; + + /** + * 业务流程关联表 分页 + */ + @PostMapping("/mkList") + @ApiOperationSupport(order = 1) + @Operation(summary = "分页", description = "传入businessProcess") + public R> mkList(@Validated @RequestBody(required = false) ApprovalDTO param, Query query) { + if (param == null) { + param = new ApprovalDTO(); + } + param.setLoginName(AuthUtil.getUserAccount()); + IPage pages = businessProcessService.queryMkApprovalList(Condition.getPage(query), param); + return R.data(pages); + } + + @GetMapping("/isEditView") + @ApiOperationSupport(order = 2) + @Operation(summary = "是否编辑页", description = "传入业务id") + public R isEditView(@Valid @NotBlank(message = "业务id不能为空") String bizId) { + return R.data(businessProcessService.isEditView(bizId)); + } + + @GetMapping("/getMKApprovalUrl") + @ApiOperationSupport(order = 3) + @Operation(summary = "获取mk审批页链接", description = "传入业务id或流程实例id") + public R getMKApprovalUrl(String bizId, String processInstanceId) { + return R.data(businessProcessService.getMKApprovalUrl(bizId, processInstanceId)); + } + + @GetMapping("/getApprovedRecords") + @ApiOperationSupport(order = 4) + @Operation(summary = "查询审批记录", description = "传入业务id或流程实例id") + public R> getApprovedRecords(String bizId, String processInstanceId) { + return R.data(businessProcessService.queryApprovedRecords(bizId, processInstanceId)); + } + + @GetMapping("/downloadFile") + @ApiOperationSupport(order = 5) + @Operation(summary = "下载附件", description = "传入附件id") + public void downloadFile(HttpServletResponse response, @Valid @NotBlank(message = "附件id不能为空") String fileId) { + businessProcessService.downloadFile(response, fileId); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/convert/ApprovalConvert.java b/blade-service/blade-system/src/main/java/org/springblade/process/convert/ApprovalConvert.java new file mode 100644 index 0000000..dcc4b18 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/convert/ApprovalConvert.java @@ -0,0 +1,51 @@ +package org.springblade.process.convert; + +import org.mapstruct.*; +import org.springblade.common.constant.DictTypeEnum; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.process.pojo.dto.ApprovalDTO; +import org.springblade.process.pojo.vo.ApprovalVO; +import org.springblade.system.cache.DictCache; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKProcessDTO; +import org.springblade.thirdparty.mk.pojo.vo.MKApprovalVO; +import org.springblade.thirdparty.mk.pojo.vo.MKProcessVO; + +import java.util.Date; +import java.util.List; + +/** + * @author bfhuange + * @since 2025/4/3 + */ +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface ApprovalConvert { + + @Mapping(source = "dynamicProps.templateNameCn", target = "templateNameCn") + @Mapping(source = "status", target = "statusStr", qualifiedByName = "statusStr") + ApprovalVO mk2vo(MKApprovalVO vo); + + List mk2vos(List vos); + + @Mapping(source = "creator", target = "applicantName") + @Mapping(source = "currentNode", target = "nodeName") + @Mapping(source = "templateName", target = "templateNameCn") + @Mapping(source = "createTime", target = "startTime") + ApprovalVO mk2vo(MKProcessVO vo); + + List mkProcess2vos(List vos); + + @Mapping(source = "docType", target = "mydoc") + @Mapping(source = "applicantTimeStart", target = "createBeginTime", qualifiedByName = "date2long") + @Mapping(source = "applicantTimeEnd", target = "createEndTime", qualifiedByName = "date2long") + MKProcessDTO dto2mk(ApprovalDTO dto); + + @Named("statusStr") + default String statusStr(String status) { + return StringUtil.isBlank(status) ? "" : DictCache.getValue(DictTypeEnum.MK_STATUS.getType(), status); + } + + @Named("date2long") + default Long date2long(Date date) { + return date == null ? null : date.getTime(); + } +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/convert/BusinessProcessConvert.java b/blade-service/blade-system/src/main/java/org/springblade/process/convert/BusinessProcessConvert.java new file mode 100644 index 0000000..d8bd781 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/convert/BusinessProcessConvert.java @@ -0,0 +1,84 @@ +package org.springblade.process.convert; + +import org.mapstruct.*; +import org.springblade.common.constant.DictTypeEnum; +import org.springblade.core.tool.utils.CollectionUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.process.pojo.dto.AdditionOperationParameterDTO; +import org.springblade.process.pojo.dto.BusinessProcessSubmitDTO; +import org.springblade.process.pojo.dto.ProcessExecuteDTO; +import org.springblade.process.pojo.entity.BusinessProcess; +import org.springblade.process.pojo.vo.BusinessProcessListVO; +import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; +import org.springblade.process.pojo.vo.ProcessAttachmentVO; +import org.springblade.process.pojo.vo.ProcessCommentVO; +import org.springblade.system.cache.DictCache; +import org.springblade.thirdparty.mk.constant.MKConstant; +import org.springblade.thirdparty.mk.pojo.dto.MKAdditionOperationParameterDTO; +import org.springblade.thirdparty.mk.pojo.dto.MKProcessExecuteDTO; +import org.springblade.thirdparty.mk.pojo.vo.MKAttachmentVO; +import org.springblade.thirdparty.mk.pojo.vo.MKAuditNoteVO; +import org.springblade.thirdparty.mk.pojo.vo.MKProcessCommentVO; +import org.springblade.thirdparty.mk.pojo.vo.MKUserOrgVO; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** + * @author bfhuange + * @date 2024/9/19 + */ +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface BusinessProcessConvert { + + BusinessProcess dto2entity(BusinessProcessSubmitDTO dto); + + MKProcessExecuteDTO dto2mk(ProcessExecuteDTO dto); + + MKAdditionOperationParameterDTO dto2mk(AdditionOperationParameterDTO dto); + + ProcessApprovedRecordVO mk2vo(MKAuditNoteVO vo); + + ProcessAttachmentVO mk2vo(MKAttachmentVO vo); + + List attachments2vos(List vos); + + @Mapping(source = "userOrgInfo", target = "userName", qualifiedByName = "userName") + ProcessCommentVO mk2vo(MKProcessCommentVO vo); + + List comments2vos(List vos); + + default List auditNotes2vos(List vos, Function> senderFunction) { + if (CollectionUtil.isEmpty(vos)) { + return Collections.emptyList(); + } + return vos.stream() + .map(auditNote -> { + ProcessApprovedRecordVO record = this.mk2vo(auditNote); + if (record != null && MKConstant.NODE_TYPE_SEND.equals(record.getNodeType())) { + // 抄送节点,查询抄送人员 + record.setSenders(senderFunction.apply(record)); + } + return record; + }).toList(); + } + + default void handleDict(BusinessProcessListVO vo) { + if (vo == null) { + return; + } + String processTypeStr = StringUtil.isBlank(vo.getProcessType()) ? "" : DictCache.getValue(DictTypeEnum.PROCESS_TYPE.getType(), vo.getProcessType()); + vo.setProcessTypeStr(processTypeStr); + String approveStatusStr = StringUtil.isBlank(vo.getApproveStatus()) ? "" : DictCache.getValue(DictTypeEnum.APPROVE_STATUS.getType(), vo.getApproveStatus()); + vo.setApproveStatusStr(approveStatusStr); + } + + @Named("userName") + default String userName(MKUserOrgVO userOrgInfo) { + return Optional.ofNullable(userOrgInfo) + .map(MKUserOrgVO::getName) + .orElse(null); + } +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/convert/MKApprovalConvert.java b/blade-service/blade-system/src/main/java/org/springblade/process/convert/MKApprovalConvert.java new file mode 100644 index 0000000..8d680a3 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/convert/MKApprovalConvert.java @@ -0,0 +1,149 @@ +package org.springblade.process.convert; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springblade.process.pojo.dto.ApprovalDTO; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKApprovalConditionDTO; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKConditionDTO; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKConditionDTO.MKConditionDTOBuilder; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * @author bfhuange + * @since 2025/4/3 + */ +@Getter +public enum MKApprovalConvert { + /** + * 单据类型 + */ + DOC_TYPE(MKApprovalConditionDTO::setMydoc, compose(MKConditionDTOBuilder::eq, ApprovalDTO::getDocType)), + /** + * 关键字 + */ + KEYWORD((condition, keyword) -> { + if (condition.getKeyword() == null) { + condition.setKeyword(new ArrayList<>()); + } + condition.getKeyword().add(keyword); + }, compose(MKConditionDTOBuilder::eq, ApprovalDTO::getKeyword)), + /** + * 模板名称 + */ + TEMPLATE_NAME(MKApprovalConditionDTO::setTemplateName, compose(MKConditionDTOBuilder::contains, ApprovalDTO::getTemplateName)), + /** + * 发起时间 + */ + START_TIME(MKApprovalConditionDTO::setStartTime, + compose(MKConditionDTOBuilder::gte, getGetter(ApprovalDTO::getApplicantTimeStart)), + compose(MKConditionDTOBuilder::lte, getGetter(ApprovalDTO::getApplicantTimeEnd)) + ), + /** + * 接收时间 + */ + RECEIVE_TIME(MKApprovalConditionDTO::setReceiveTime, + compose(MKConditionDTOBuilder::gte, getGetter(ApprovalDTO::getReceiveTimeStart)), + compose(MKConditionDTOBuilder::lte, getGetter(ApprovalDTO::getReceiveTimeEnd)) + ), + /** + * 流程状态 + */ + STATUS(MKApprovalConditionDTO::setStatus, compose(MKConditionDTOBuilder::eq, ApprovalDTO::getStatus)), + /** + * 结束时间 + */ + FINISH_TIME(MKApprovalConditionDTO::setFinishTime, + compose(MKConditionDTOBuilder::gte, getGetter(ApprovalDTO::getFinishTimeStart)), + compose(MKConditionDTOBuilder::lte, getGetter(ApprovalDTO::getFinishTimeEnd)) + ), + /** + * 最后处理时间 + */ + LAST_HANDLE_TIME(MKApprovalConditionDTO::setLastHandleTime, + compose(MKConditionDTOBuilder::gte, getGetter(ApprovalDTO::getLastHandleStart)), + compose(MKConditionDTOBuilder::lte, getGetter(ApprovalDTO::getLastHandleEnd)) + ), + /** + * 阅读时间 + */ + READ_TIME(MKApprovalConditionDTO::setReadTime, + compose(MKConditionDTOBuilder::gte, getGetter(ApprovalDTO::getReadTimeStart)), + compose(MKConditionDTOBuilder::lte, getGetter(ApprovalDTO::getReadTimeEnd)) + ), + /** + * 创建时间 + */ + CREATE_TIME(MKApprovalConditionDTO::setCreateTime, + compose(MKConditionDTOBuilder::gte, getGetter(ApprovalDTO::getCreateTimeStart)), + compose(MKConditionDTOBuilder::lte, getGetter(ApprovalDTO::getCreateTimeEnd)) + ), + ; + /** + * 最终设置条件方法 + */ + private final BiConsumer setter; + /** + * 组合参数 + */ + private final List composes; + + MKApprovalConvert(BiConsumer setter, Compose... compose) { + this.setter = setter; + this.composes = List.of(compose); + } + + /** + * 获取时间戳 + * @param date + * @return + */ + private static String getTimestamp(Date date) { + return Optional.ofNullable(date) + .map(e -> String.valueOf(e.getTime())) + .orElse(null); + } + + /** + * date 转 string + * @param dateGetter + * @return + */ + private static Function getGetter(Function dateGetter) { + return approvalDTO -> getTimestamp(dateGetter.apply(approvalDTO)); + } + + /** + * 工厂方法 + * @param builderSetter + * @param getter + * @return + */ + private static Compose compose(BiConsumer builderSetter, Function getter) { + return new Compose(builderSetter, getter); + } + + /** + * 组合参数,条件和取值 + */ + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class Compose { + /** + * 条件builder的setter + */ + private BiConsumer builderSetter; + /** + * 从参数取值 + */ + private Function getter; + } +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java new file mode 100644 index 0000000..e8ca2df --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java @@ -0,0 +1,77 @@ +package org.springblade.process.feign; + +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springblade.core.tool.api.FR; +import org.springblade.process.pojo.dto.BusinessProcessCurrentHandlerRefreshDTO; +import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO; +import org.springblade.process.pojo.dto.BusinessProcessSubmitDTO; +import org.springblade.process.pojo.dto.BusinessProcessUpdateDTO; +import org.springblade.process.pojo.vo.BusinessProcessVO; +import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; +import org.springblade.process.pojo.vo.ProcessTodoVO; +import org.springblade.process.service.IBusinessProcessService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 业务流程关联表 Feign实现类 + * + * @author BladeX + * @since 2024-09-19 + */ +@Valid +@Hidden +@RestController +@AllArgsConstructor +public class BusinessProcessClient implements IBusinessProcessClient { + + private final IBusinessProcessService businessProcessService; + + @PostMapping(SUBMIT_BUSINESS_PROCESS) + @Override + public FR submitBusinessProcess(@Validated @RequestBody BusinessProcessSubmitDTO param) { + return FR.data(businessProcessService.submitBusinessProcess(param)); + } + + @Override + public FR updateBusinessProcessStatus(@Validated @RequestBody BusinessProcessUpdateDTO param) { + return FR.data(businessProcessService.updateBusinessProcessStatus(param)); + } + + @Override + public FR updateBusinessProcessApprover(@Validated @RequestBody BusinessProcessUpdateDTO param) { + return FR.data(businessProcessService.updateBusinessProcessApprover(param)); + } + + @Override + public FR refreshBusinessProcessCurrentHandlers(@Validated @RequestBody BusinessProcessCurrentHandlerRefreshDTO param) { + return FR.data(businessProcessService.refreshBusinessProcessCurrentHandlers(param)); + } + + @Override + public FR> queryTodoList(String processInstanceId) { + return FR.data(businessProcessService.queryTodoList(processInstanceId)); + } + + @Override + public FR queryBusinessProcessSnapshot(String processInstanceId) { + return FR.data(businessProcessService.queryBusinessProcessSnapshot(processInstanceId)); + } + + @PostMapping(DELETE_BUSINESS_PROCESS) + @Override + public FR deleteBusinessProcess(@Validated @RequestBody BusinessProcessDeleteDTO param) { + return FR.data(businessProcessService.deleteBusinessProcess(param)); + } + + @Override + public FR> queryApprovedRecordsNoAttachments(String bizId, String processInstanceId) { + return FR.data(businessProcessService.queryApprovedRecordsNoAttachments(bizId, processInstanceId)); + } +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.java b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.java new file mode 100644 index 0000000..3ef7fdf --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.java @@ -0,0 +1,14 @@ +package org.springblade.process.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.process.pojo.entity.BusinessProcess; + +/** + * 业务流程关联表 Mapper 接口 + * + * @author BladeX + * @since 2024-09-19 + */ +public interface BusinessProcessMapper extends BaseMapper { + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml new file mode 100644 index 0000000..47b45b7 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java new file mode 100644 index 0000000..9dd744d --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java @@ -0,0 +1,115 @@ +package org.springblade.process.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import jakarta.servlet.http.HttpServletResponse; +import org.springblade.process.pojo.dto.*; +import org.springblade.process.pojo.entity.BusinessProcess; +import org.springblade.process.pojo.vo.*; + +import java.util.List; + +/** + * 业务流程关联表 服务类 + * + * @author BladeX + * @since 2024-09-19 + */ +public interface IBusinessProcessService extends IService { + + /** + * 提交业务流程 + * + * @param param + * @return + */ + BusinessProcessVO submitBusinessProcess(BusinessProcessSubmitDTO param); + + /** + * 修改业务流程状态 + * @param param + * @return + */ + boolean updateBusinessProcessStatus(BusinessProcessUpdateDTO param); + + /** + * 修改业务流程审批人 + * @param param + * @return + */ + BusinessProcessVO updateBusinessProcessApprover(BusinessProcessUpdateDTO param); + + /** + * 只刷新当前节点和当前处理人 + * @param param + * @return + */ + BusinessProcessVO refreshBusinessProcessCurrentHandlers(BusinessProcessCurrentHandlerRefreshDTO param); + + /** + * 查询业务流程当前快照 + * @param processInstanceId + * @return + */ + BusinessProcessVO queryBusinessProcessSnapshot(String processInstanceId); + + /** + * 是否编辑页 + * @param bizId + * @return + */ + boolean isEditView(String bizId); + + /** + * 获取mk审批页面链接,业务id和流程实例id任意一个即可 + * @param bizId 业务id + * @param processInstanceId 流程实例id + * @return + */ + String getMKApprovalUrl(String bizId, String processInstanceId); + + /** + * 删除业务流程 + * @param param + * @return + */ + boolean deleteBusinessProcess(BusinessProcessDeleteDTO param); + + /** + * 查询流程审批记录 + * @param bizId + * @param processInstanceId + * @return + */ + List queryApprovedRecords(String bizId, String processInstanceId); + + /** + * 查询流程审批记录不处理附件 + * @param bizId + * @param processInstanceId + * @return + */ + List queryApprovedRecordsNoAttachments(String bizId, String processInstanceId); + + /** + * 下载文件 + * @param response + * @param fileId + */ + void downloadFile(HttpServletResponse response, String fileId); + + /** + * 查询业务流程当前处理人 + * @param processInstanceId + * @return + */ + List queryTodoList(String processInstanceId); + + /** + * 查询mk审批记录 + * @param page + * @param param + * @return + */ + IPage queryMkApprovalList(IPage page, ApprovalDTO param); +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java new file mode 100644 index 0000000..105651e --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java @@ -0,0 +1,725 @@ +package org.springblade.process.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springblade.process.pojo.dto.*; +import org.springblade.process.pojo.enums.ApproveStatusEnum; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.log.utils.AssertUtils; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.process.convert.ApprovalConvert; +import org.springblade.process.convert.BusinessProcessConvert; +import org.springblade.process.convert.MKApprovalConvert; +import org.springblade.process.mapper.BusinessProcessMapper; +import org.springblade.process.pojo.entity.BusinessProcess; +import org.springblade.process.pojo.enums.TodoStatus; +import org.springblade.process.pojo.vo.*; +import org.springblade.process.service.IBusinessProcessService; +import org.springblade.thirdparty.mk.config.MKProperties; +import org.springblade.thirdparty.mk.constant.MKConstant; +import org.springblade.thirdparty.mk.constant.MKDoc; +import org.springblade.thirdparty.mk.exception.MKException; +import org.springblade.thirdparty.mk.pojo.dto.MKAuditNoteDTO; +import org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO; +import org.springblade.thirdparty.mk.pojo.dto.MKProcessExecuteDTO; +import org.springblade.thirdparty.mk.pojo.dto.MKSenderDTO; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKApprovalConditionDTO; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKApprovalDTO; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKConditionDTO; +import org.springblade.thirdparty.mk.pojo.dto.approval.MKProcessDTO; +import org.springblade.thirdparty.mk.pojo.dto.sort.*; +import org.springblade.thirdparty.mk.pojo.vo.*; +import org.springblade.thirdparty.mk.service.IMKService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +/** + * 业务流程关联表 服务实现类 + * + * @author BladeX + * @since 2024-09-19 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class BusinessProcessServiceImpl extends ServiceImpl implements IBusinessProcessService { + + private final BusinessProcessConvert convert; + private final IMKService mkService; + private final MKProperties mkProperties; + private final ApprovalConvert approvalConvert; + + @Transactional(rollbackFor = Exception.class) + @Override + public BusinessProcessVO submitBusinessProcess(BusinessProcessSubmitDTO param) { + if (param == null) { + log.warn("提交业务流程参数为空"); + return null; + } + Long bizId = param.getBizId(); + if (bizId == null) { + log.warn("提交业务流程业务id为空"); + return null; + } + log.info("提交业务流程参数:{}", JSON.toJSONString(param)); + BusinessProcess businessProcess = this.getOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + ); + if (businessProcess == null) { + businessProcess = convert.dto2entity(param); + } + // 设置发起人 + if (businessProcess.getPromoterId() == null) { + businessProcess.setPromoterId(AuthUtil.getUserId()); + } + if (businessProcess.getPromoterName() == null) { + businessProcess.setPromoterName(AuthUtil.getNickName()); + } + if (businessProcess.getPromoterLoginName() == null) { + businessProcess.setPromoterLoginName(AuthUtil.getUserName()); + param.setPromoterLoginName(AuthUtil.getUserName()); + } + // 设置提交时间 + if (businessProcess.getSubmitTime() == null) { + businessProcess.setSubmitTime(new Date()); + } + // 1.提交流程 + long start = System.currentTimeMillis(); + log.info("提交流程开始"); + String processInstanceId = submitMKProcess(param); + long end = System.currentTimeMillis(); + log.info("提交流程结束 耗时:{}", end - start); + businessProcess.setProcessInstanceId(processInstanceId); + // 提交是审批中状态 + businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue()); + // 2.保存业务流程 + this.saveOrUpdate(businessProcess); + + // 3.查询当前节点 + BusinessProcessVO businessProcessVO = new BusinessProcessVO(); + businessProcessVO.setProcessInstanceId(processInstanceId); + return businessProcessVO; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateBusinessProcessStatus(BusinessProcessUpdateDTO param) { + AssertUtils.notNull(param, "参数不能为空"); + BusinessProcess businessProcess = this.getOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getProcessInstanceId, param.getProcessInstanceId()) + ); + AssertUtils.notNull(businessProcess, "流程实例不存在"); + // if (StringUtils.isNotBlank(approveStatus) && !rejectAfterPass(approveStatus, operationNodeNumber)) { + if (StringUtils.isNotBlank(param.getApproveStatus()) && updateApproveStatus(param.getApproveStatus(), param.getRejectNodeId())) { + // 审批状态不为空且需要修改审批状态 + BusinessProcess updateParam = new BusinessProcess(); + updateParam.setId(businessProcess.getId()); + updateParam.setApproveStatus(param.getApproveStatus()); + return this.updateById(updateParam); + } + return true; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public BusinessProcessVO updateBusinessProcessApprover(BusinessProcessUpdateDTO param) { + BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(param.getProcessInstanceId()); + String promoterLoginName = resolvePromoterLoginName(businessProcess, param.getPromoterLoginName()); + if (StringUtils.isBlank(promoterLoginName)) { + log.warn("修改业务流程审批人失败,发起人登录名为空,流程实例id:{}", param.getProcessInstanceId()); + return null; + } + BusinessProcessVO businessProcessVO = this.refreshBusinessProcessCurrentHandlers(param); + if (businessProcessVO == null) { + return null; + } + // 再补历史已办逻辑,兼容旧代码 + if (param.getOperationNodeId() != null && !MKConstant.DAFTER_NODE_ID.equals(param.getOperationNodeId())) { + MKAllHandlerVO nodeHandlers = mkService.getNodeHandlers(param.getProcessInstanceId(), promoterLoginName, param.getOperationNodeId()); + this.handleNodeHandlers(param.getProcessInstanceId(), nodeHandlers, param); + } + return businessProcessVO; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public BusinessProcessVO refreshBusinessProcessCurrentHandlers(BusinessProcessCurrentHandlerRefreshDTO param) { + if (param == null) { + log.warn("刷新业务流程当前处理人参数为空"); + return null; + } + BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(param.getProcessInstanceId()); + String promoterLoginName = resolvePromoterLoginName(businessProcess, param.getPromoterLoginName()); + if (StringUtils.isBlank(promoterLoginName)) { + log.warn("刷新业务流程当前处理人失败,发起人登录名为空,流程实例id:{}", param.getProcessInstanceId()); + return null; + } + Long businessProcessId = Optional.ofNullable(businessProcess).map(BusinessProcess::getId).orElse(null); + // 1. 查询当前节点 + BusinessProcessVO businessProcessVO = new BusinessProcessVO(); + businessProcessVO.setProcessInstanceId(param.getProcessInstanceId()); + List currentNodes = mkService.getCurrentNodes(param.getProcessInstanceId(), promoterLoginName); + // 处理当前节点信息 + BusinessProcess updateBusinessProcess = this.handleCurrentNodes(businessProcessId, param.getProcessInstanceId(), param.isComplete(), currentNodes, businessProcessVO); + if (updateBusinessProcess != null) { + // 设置当前处理人、当前节点、接收时间 + businessProcessVO.setCurrentHandlers(updateBusinessProcess.getCurrentHandlers()); + businessProcessVO.setCurrentNodeIds(updateBusinessProcess.getCurrentNodeIds()); + businessProcessVO.setCurrentNodeNames(updateBusinessProcess.getCurrentNodeNames()); + businessProcessVO.setReceiveTime(updateBusinessProcess.getReceiveTime()); + } + return businessProcessVO; + } + + @Override + public BusinessProcessVO queryBusinessProcessSnapshot(String processInstanceId) { + AssertUtils.notBlank(processInstanceId, "流程实例id不能为空"); + BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId); + AssertUtils.notNull(businessProcess, "业务流程不存在"); + return this.buildBusinessProcessVO(businessProcess); + } + + @Override + public boolean isEditView(String bizId) { + if (StringUtils.isBlank(bizId)) { + log.warn("查询是否编辑页,业务id为空"); + return false; + } + BusinessProcess businessProcess = this.baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + .last("limit 1") + ); + if (businessProcess == null) { + log.warn("查询是否编辑页,业务流程不存在 业务id:{}", bizId); + return false; + } + String approveStatus = businessProcess.getApproveStatus(); + String userAccount = AuthUtil.getUserAccount(); + String promoterLoginName = businessProcess.getPromoterLoginName(); + // (驳回或撤销或草稿)且当前登录人是流程提交人 + boolean result = ApproveStatusEnum.canEdit(approveStatus) && userAccount.equals(promoterLoginName); + log.info("是否编辑页 审批状态:{} 当前登录人:{} 提交人:{} 结果:{}", approveStatus, userAccount, promoterLoginName, result); + return result; + } + + @Override + public String getMKApprovalUrl(String bizId, String processInstanceId) { + boolean bizIdBlank = StringUtil.isBlank(bizId); + boolean processInstanceIdBlank = StringUtil.isBlank(processInstanceId); + AssertUtils.isFalse(bizIdBlank && processInstanceIdBlank, "参数不能为空"); + if (processInstanceIdBlank) { + BusinessProcess businessProcess = this.getOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + .last("limit 1") + ); + AssertUtils.notNull(businessProcess, "业务流程不存在"); + processInstanceId = businessProcess.getProcessInstanceId(); + } + try { + String mkApprovalUrl = mkService.getMKApprovalUrl(processInstanceId, AuthUtil.getUserAccount()); + AssertUtils.notBlank(mkApprovalUrl, "获取mk审批页面链接异常"); + return mkApprovalUrl; + } catch (MKException e) { + throw new ServiceException("获取mk审批页面链接异常 " + e.getMessage()); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean deleteBusinessProcess(BusinessProcessDeleteDTO param) { + log.info("删除业务流程 参数:{}", JSON.toJSONString(param)); + Long bizId = param.getBizId(); + String promoterLoginName = param.getPromoterLoginName(); + if (bizId == null) { + return false; + } + BusinessProcess businessProcess = this.getOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + ); + if (businessProcess == null) { + log.warn("业务流程不存在 业务id:{}", bizId); + return false; + } + if (promoterLoginName == null) { + promoterLoginName = businessProcess.getPromoterLoginName(); + } + // 1. 删除业务流程 + this.removeById(businessProcess.getId()); + // 2. 删除待办 + if (StringUtils.isEmpty(businessProcess.getProcessInstanceId())) { + log.warn("流程id为空 id:{} 业务id:{}", businessProcess.getId(), bizId); + return true; + } + // 3. 删除流程 + return mkService.processDelete(businessProcess.getProcessInstanceId(), promoterLoginName); + } + + @Override + public List queryApprovedRecords(String bizId, String processInstanceId) { + List records = this.queryApprovedRecordsNoAttachments(bizId, processInstanceId); + if (CollectionUtil.isEmpty(records)) { + return records; + } + Map fileMap = new HashMap<>(); + for (ProcessApprovedRecordVO record : records) { + List attachmentParameter = record.getAttachmentParameter(); + // 处理电子签名base64并排序附件 + attachmentParameter = handleAttachmentBase64(attachmentParameter, fileMap); + record.setAttachmentParameter(attachmentParameter); + if (CollectionUtil.isNotEmpty(record.getProcessComments())) { + for (ProcessCommentVO processComment : record.getProcessComments()) { + // 处理电子签名base64并排序附件 + List attachments = handleAttachmentBase64(processComment.getAttachments(), fileMap); + processComment.setAttachments(attachments); + } + } + } + return records; + } + + @Override + public List queryApprovedRecordsNoAttachments(String bizId, String processInstanceId) { + boolean bizIdBlank = StringUtil.isBlank(bizId); + boolean processInstanceIdBlank = StringUtil.isBlank(processInstanceId); + AssertUtils.isFalse(bizIdBlank && processInstanceIdBlank, "参数不能为空"); + + BusinessProcess businessProcess = this.getOne(Wrappers.lambdaQuery() + .eq(!bizIdBlank, BusinessProcess::getBizId, bizId) + .eq(!processInstanceIdBlank, BusinessProcess::getProcessInstanceId, processInstanceId) + .last("limit 1") + ); + AssertUtils.notNull(businessProcess, "业务流程不存在"); + if (processInstanceIdBlank) { + processInstanceId = businessProcess.getProcessInstanceId(); + } + // 查询审批记录 + List mkAuditNotes = mkService.queryAuditNotes(new MKAuditNoteDTO(businessProcess.getPromoterLoginName(), processInstanceId)); + // 转换参数 + return convert.auditNotes2vos(mkAuditNotes, record -> { + List mkSenders = mkService.querySenderList(new MKSenderDTO(record.getProcessInstanceId(), record.getNodeInstanceId())); + return mkSenders.stream() + .map(MKSenderVO::getName) + .toList(); + }); + } + + /** + * 处理附件base64并排序附件 + * @param attachmentParameter + * @param fileMap + */ + private List handleAttachmentBase64(List attachmentParameter, Map fileMap) { + if (CollectionUtil.isEmpty(attachmentParameter)) { + return attachmentParameter; + } + // 电子签名的附件 + List signAttachments = attachmentParameter.stream() + .filter(attachment -> MKConstant.FILE_TYPE_SIGN.equals(attachment.getType())) + .peek(attachment -> { + // 电子签名,查询图片base64 + if (fileMap.containsKey(attachment.getFileId())) { + attachment.setBase64(fileMap.get(attachment.getFileId())); + } else { + String fileBase64 = mkService.getFileBase64(attachment.getFileId()); + fileMap.put(attachment.getFileId(), fileBase64); + attachment.setBase64(fileBase64); + } + }).toList(); + if (CollectionUtil.isEmpty(signAttachments)) { + // 没有电子签名的附件,无需处理 + return attachmentParameter; + } + List result = new ArrayList<>(); + // 纯附件,非电子签名附件 + List attachments = attachmentParameter.stream() + .filter(attachment -> !MKConstant.FILE_TYPE_SIGN.equals(attachment.getType())) + .toList(); + if (CollectionUtil.isNotEmpty(attachments)) { + result.addAll(attachments); + } + // 把电子签名附件放到最后 + result.addAll(signAttachments); + return result; + } + + @Override + public void downloadFile(HttpServletResponse response, String fileId) { + mkService.downloadFile(response,fileId); + } + + @Override + public List queryTodoList(String processInstanceId) { + AssertUtils.notNull(processInstanceId, "流程实例id不能为空"); + BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId); + if (businessProcess == null) { + return null; + } + // 1. 查询当前节点处理人 + List currentNodes = mkService.getCurrentNodes(processInstanceId, businessProcess.getPromoterLoginName()); + if (CollectionUtil.isEmpty(currentNodes)) { + return Collections.emptyList(); + } + return getProcessTodoList(currentNodes); + } + + /** + * 通过流程实例id查询业务流程 + * @param processInstanceId 流程实例id + * @return 业务流程 + */ + private BusinessProcess getBusinessProcessByProcessInstanceId(String processInstanceId) { + return this.getOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getProcessInstanceId, processInstanceId) + ); + } + + /** + * 解析最终使用的发起人登录名 + * @param businessProcess 业务流程 + * @param fallbackPromoterLoginName 调用方传入的发起人登录名 + * @return 发起人登录名 + */ + private String resolvePromoterLoginName(BusinessProcess businessProcess, String fallbackPromoterLoginName) { + return Optional.ofNullable(businessProcess) + .map(BusinessProcess::getPromoterLoginName) + .filter(StringUtils::isNotBlank) + .orElse(fallbackPromoterLoginName); + } + + /** + * 构造业务流程快照 + * @param businessProcess 业务流程 + * @return 快照 + */ + private BusinessProcessVO buildBusinessProcessVO(BusinessProcess businessProcess) { + BusinessProcessVO businessProcessVO = new BusinessProcessVO(); + businessProcessVO.setProcessInstanceId(businessProcess.getProcessInstanceId()); + businessProcessVO.setCurrentNodeIds(businessProcess.getCurrentNodeIds()); + businessProcessVO.setCurrentNodeNames(businessProcess.getCurrentNodeNames()); + businessProcessVO.setCurrentHandlers(businessProcess.getCurrentHandlers()); + businessProcessVO.setReceiveTime(businessProcess.getReceiveTime()); + return businessProcessVO; + } + + /** + * 获取流程待办列表 + * @param currentNodes + * @return + */ + private List getProcessTodoList(List currentNodes) { + if (CollectionUtil.isEmpty(currentNodes)) { + return Collections.emptyList(); + } + return currentNodes.stream() + .filter(node -> CollectionUtil.isNotEmpty(node.getNodeHandlers())) + .flatMap(node -> node.getNodeHandlers().stream() + // 过滤掉登录名为空的脏数据 + .filter(handler -> handler.getFdHandlerOrgInfo() != null && StringUtil.isNotBlank(handler.getFdHandlerOrgInfo().getLoginName())) + .map(handler -> { + ProcessTodoVO addParam = new ProcessTodoVO(); + addParam.setProcessInstanceId(node.getProcessInstanceId()); + addParam.setNodeId(node.getNodeId()); + addParam.setNodeNumber(node.getNodeNumber()); + addParam.setNodeName(node.getNodeName()); + addParam.setLoginName(handler.getFdHandlerOrgInfo().getLoginName()); + addParam.setUserName(handler.getHandlerName()); + addParam.setStatus(TodoStatus.TODO.getCode()); + addParam.setReceiveTime(handler.getReceiveTime()); + return addParam; + }) + ).toList(); + } + + @Override + public IPage queryMkApprovalList(IPage page, ApprovalDTO param) { + if (MKDoc.RELATED.getCode().equals(param.getDocType())) { + // 我参与的,调用流程列表接口 + MKProcessDTO processParam = approvalConvert.dto2mk(param); + processParam.setPage((int) page.getCurrent(), (int) page.getSize()); + MKPageVO mkPage = mkService.queryProcessList(processParam); + page.setTotal(mkPage.getTotalSize()); + page.setRecords(approvalConvert.mkProcess2vos(mkPage.getContent())); + return page; + } + // 非我参与的,调用审批中心接口 + MKApprovalDTO approvalParam = getMkApprovalParam(param); + approvalParam.setPage((int) page.getCurrent(), (int) page.getSize()); + MKPageVO mkPage = mkService.queryApprovalList(approvalParam); + page.setTotal(mkPage.getTotalSize()); + page.setRecords(approvalConvert.mk2vos(mkPage.getContent())); + return page; + } + + /** + * 获取mk查询参数 + * + * @param param + * @return + */ + private MKApprovalDTO getMkApprovalParam(ApprovalDTO param) { + String docType = param.getDocType(); + // 我的待审 + // mk页面接口参数 {"offset":0,"pageNo":1,"pageSize":10,"conditions":{"fdStartTime":{"$gte":1711900800000,"$lte":1746374399999},"fdReceiveTime":{"$gte":1712160000000,"$lte":1746115199999},"fdTemplateName":{"$contains":"测试"},"keyword":{"$eq":"提交"},"mydoc":{"$eq":"myApproving"}},"sorts":{"fdLevel":"asc","fdReceiveTime":"desc"}} + MKApprovalDTO approvalParam = new MKApprovalDTO(); + approvalParam.setLoginName(param.getLoginName()); + ISort sort = getSort(docType, approvalParam); + approvalParam.setSorts(sort); + // 获取查询条件的参数 + MKApprovalConditionDTO condition = getCondition(param); + approvalParam.setConditions(condition); + return approvalParam; + } + + /** + * 获取排序 + * @param docType + * @param approvalParam + * @return + */ + private ISort getSort(String docType, MKApprovalDTO approvalParam) { + if (MKDoc.APPROVING.getCode().equals(docType) || MKDoc.READING.getCode().equals(docType)) { + // 待办、待阅排序是相同的 + return new MKApprovingSortDTO(); + } + if (MKDoc.APPROVED.getCode().equals(docType)) { + // 已办 + return new MKApprovedSortDTO(); + } + if (MKDoc.READ.getCode().equals(docType)) { + // 已阅 + return new MKReadSortDTO(); + } + if (MKDoc.CREATE.getCode().equals(docType) || MKDoc.RELATED.getCode().equals(docType)) { + // 我发起的/我关联的 + return new MKCreateSortDTO(); + } + throw new ServiceException("不支持的单据类型"); + } + + /** + * 获取查询条件 + * @param param + * @return + */ + private MKApprovalConditionDTO getCondition(ApprovalDTO param) { + MKApprovalConditionDTO condition = null; + for (MKApprovalConvert convert : MKApprovalConvert.values()) { + MKConditionDTO.MKConditionDTOBuilder builder = null; + BiConsumer setter = convert.getSetter(); + List composes = convert.getComposes(); + // 是否多个 Compose + boolean multi = composes.size() > 1; + if (multi) { + // 不是多个setter + for (MKApprovalConvert.Compose compose : composes) { + // 遍历获取参数值 + String value = compose.getGetter().apply(param); + if (StringUtils.isNotBlank(value)) { + // 参数值不为空,设置到builder + if (builder == null) { + builder = MKConditionDTO.builder(); + } + compose.getBuilderSetter().accept(builder, value); + } + } + if (builder != null) { + // builder不为空,设置到最终的条件 + if (condition == null) { + condition = new MKApprovalConditionDTO(); + } + setter.accept(condition, builder.build()); + convert.getSetter().accept(condition, builder.build()); + } + } else { + // 只有1个compose + MKApprovalConvert.Compose compose = composes.get(0); + String value = compose.getGetter().apply(param); + if (StringUtils.isNotBlank(value)) { + // 替换中文逗号为英文逗号 + value = value.replace(",", ","); + // 按英文逗号拆分值 + String[] values = value.split(","); + for (String singleValue: values) { + if (StringUtils.isNotBlank(value)) { + // 参数值不为空,设置到builder + if (builder == null) { + builder = MKConditionDTO.builder(); + } + compose.getBuilderSetter().accept(builder, singleValue); + if (builder != null) { + // builder不为空,设置到最终的条件 + if (condition == null) { + condition = new MKApprovalConditionDTO(); + } + // 索引不超过setters长度,设置条件 + setter.accept(condition, builder.build()); + } + } + } + } + } + } + return condition; + } + + /** + * 处理已审批的人 + * + * @param processInstanceId + * @param nodeHandlers + * @param param + */ + private void handleNodeHandlers(String processInstanceId, MKAllHandlerVO nodeHandlers, BusinessProcessUpdateDTO param) { + if (nodeHandlers == null) { + log.warn("修改业务流程 查询操作节点历史处理信息为空 流程实例id:{} 操作节点id:{}", param.getProcessInstanceId(), param.getOperationNodeId()); + return; + } + List approvedHandlers = nodeHandlers.getApprovedHandlers(); + if (CollectionUtil.isEmpty(approvedHandlers)) { + // 已审批为空 + log.warn("修改业务流程 查询操作节点已处理信息为空 流程实例id:{} 操作节点id:{}", param.getProcessInstanceId(), param.getOperationNodeId()); + return; + } + + } + + /** + * 处理当前节点信息 + * + * @param businessProcessId + * @param processInstanceId + * @param complete + * @param currentNodes + * @param businessProcessVO + */ + private BusinessProcess handleCurrentNodes(Long businessProcessId, String processInstanceId, boolean complete, List currentNodes, BusinessProcessVO businessProcessVO) { + if (CollectionUtil.isEmpty(currentNodes)) { + log.info("修改业务流程 当前节点处理人为空"); + if (businessProcessId != null) { + // 清空当前节点,当前处理人,接收时间 + log.info("修改业务流程 流程结束清空当前节点,当前处理人,接收时间 业务流程id:{} 流程实例id:{}", businessProcessId, processInstanceId); + this.lambdaUpdate() + .eq(BusinessProcess::getId, businessProcessId) + .set(BusinessProcess::getCurrentNodeIds, null) + .set(BusinessProcess::getCurrentNodeNames, null) + .set(BusinessProcess::getCurrentHandlers, null) + .set(complete, BusinessProcess::getIsCompleted, true) + .set(complete, BusinessProcess::getCompleteTime, new Date()) + .set(BusinessProcess::getUpdateTime, new Date()) + .update(); + } else { + log.warn("修改业务流程 流程结束 业务流程id为空"); + } + return null; + } + // 处理待办 + List processTodoList = getProcessTodoList(currentNodes); + + // 更新业务流程 + return updateBusinessProcess(businessProcessId, processTodoList); + } + + /** + * 更新业务流程 + * + * @param businessProcessId + * @param addToDos + */ + private BusinessProcess updateBusinessProcess(Long businessProcessId, List addToDos) { + if (CollectionUtil.isEmpty(addToDos)) { + log.warn("新增待办为空"); + return null; + } + String nodeIds = addToDos.stream() + .map(ProcessTodoVO::getNodeId) + .distinct() + .collect(Collectors.joining(",")); + String nodeNames = addToDos.stream() + .map(ProcessTodoVO::getNodeName) + .distinct() + .collect(Collectors.joining(",")); + String usernames = addToDos.stream() + .map(ProcessTodoVO::getUserName) + .distinct() + .collect(Collectors.joining(",")); + Date receiveTime = addToDos.get(0).getReceiveTime(); + // 更新当前节点id,当前节点名称,当前处理人,接收时间 + BusinessProcess updateParam = new BusinessProcess(); + updateParam.setId(businessProcessId); + updateParam.setCurrentNodeIds(nodeIds); + updateParam.setCurrentNodeNames(nodeNames); + updateParam.setCurrentHandlers(usernames); + updateParam.setReceiveTime(receiveTime); + if (businessProcessId != null) { + this.updateById(updateParam); + } else { + log.warn("新增待办,业务流程id为空"); + } + return updateParam; + } + + /** + * 是否修改审批状态,驳回状态只修改驳回节点是起草节点的 + * @param approveStatus + * @param rejectNodeId + * @return + */ + private boolean updateApproveStatus(String approveStatus, String rejectNodeId) { + if (!ApproveStatusEnum.REJECTED.getValue().equals(approveStatus)) { + // 不是驳回状态,直接修改 + return true; + } + if (StringUtils.isBlank(rejectNodeId)) { + // 驳回节点id为空说明是老流程,没有配置参数,可以修改 + return true; + } + // 是驳回状态,只修改驳回节点id是起草节点id的 + return MKConstant.DAFTER_NODE_ID.equals(rejectNodeId); + } + + /** + * 提交mk流程 + * @param param + * @return + */ + private String submitMKProcess(BusinessProcessSubmitDTO param) { + if (param.getExecuteParam() == null || StringUtil.isBlank(param.getExecuteParam().getProcessId())) { + // 执行参数为空,是提交 + MKProcessCreateDTO processParam = new MKProcessCreateDTO(); + processParam.setFormInstanceId(String.valueOf(param.getBizId())); + processParam.setLoginName(param.getPromoterLoginName()); + processParam.setSubmitIdentity(param.getPromoterLoginName()); + processParam.setSubject(param.getSubject()); + processParam.setTemplateCode(mkProperties.getTemplateCodePrefix() + param.getProcessType()); + processParam.setFormValues(param.getProcessParam()); + // 临时变量设置为业务表单对象,以便不用修改mk流程表单字段,直接使用流程模板临时变量 + processParam.setTempVarData(ObjectUtil.cloneByStream(param.getProcessParam())); + return mkService.processSubmit(processParam); + } + // 执行参数不为空,是驳回/撤销后提交/废弃 + ProcessExecuteDTO executeParam = param.getExecuteParam(); + MKProcessExecuteDTO processExecuteDTO = convert.dto2mk(executeParam); + processExecuteDTO.setLoginName(param.getPromoterLoginName()); + processExecuteDTO.setFormValues(param.getProcessParam()); + // 临时变量设置为业务表单对象,以便不用修改mk流程表单字段,直接使用流程模板临时变量 + processExecuteDTO.setTempVarData(ObjectUtil.cloneByStream(param.getProcessParam())); + // 重新设置标题,防止标题变了 + processExecuteDTO.setSubject(param.getSubject()); + mkService.processExecute(processExecuteDTO); + return executeParam.getProcessId(); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java index 278ef63..b82811e 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java @@ -28,11 +28,13 @@ package org.springblade.system; import org.springblade.core.cloud.client.BladeCloudApplication; import org.springblade.core.launch.BladeApplication; import org.springblade.core.launch.constant.AppConstant; +import org.springframework.context.annotation.ComponentScan; /** * 系统模块启动器 * @author Chill */ +@ComponentScan(basePackages = {"org.springblade.system", "org.springblade.process"}) @BladeCloudApplication public class SystemApplication { diff --git a/pom.xml b/pom.xml index 41c2daa..29b91ad 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,11 @@ blade-system-api ${revision} + + org.springblade + blade-process-api + ${revision} + org.springblade From 0b8aecc49b048566b0f5dbfbad9a72679591b3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Sun, 9 Aug 2026 23:31:39 +0800 Subject: [PATCH 06/12] =?UTF-8?q?feat(openapi):=20=E6=B7=BB=E5=8A=A0mk?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springblade/openapi/mk/api/IApi4MK.java | 27 - .../mk/constant/ProcessLockKeyConstant.java | 30 + .../mk/pojo/dto/Api4MKProcessApprovalDTO.java | 9 - .../mk/pojo/dto/ApiMKProcessFinishDTO.java | 34 - .../mk/pojo/dto/ProcessApprovalDTO.java | 40 - .../process/feign/IBusinessProcessClient.java | 4 +- .../process/pojo/dto/ProcessExecuteDTO.java | 1 - blade-service/blade-openapi/pom.xml | 4 + .../openapi/OpenApiApplication.java | 1 + .../org/springblade/openapi/mk/Api4MK.java | 93 ++ .../mk/config/AsyncExecutorProperties.java | 27 + .../CurrentHandlerRefreshProperties.java | 52 + .../base/AbstractProcessOperationHandler.java | 301 ++++++ .../mk/support/base/ProcessHandler.java | 27 + .../support/base/ProcessOperationContext.java | 139 +++ .../support/base/ProcessOperationHandler.java | 45 + .../mk/support/handler/AsyncService.java | 113 +++ .../ProcessCurrentHandlerRefreshService.java | 903 ++++++++++++++++++ .../ProcessCurrentHandlerRefreshTask.java | 80 ++ .../openapi/mk/support/handler/TaskState.java | 25 + .../openapi/mk/util/ProcessTypeUtils.java | 23 + .../src/main/resources/application.yml | 27 + .../src/main/resources/bootstrap-dev.yml | 6 - .../src/main/resources/bootstrap-prod.yml | 6 - .../src/main/resources/bootstrap-test.yml | 8 - .../src/main/resources/bootstrap.yml | 34 - .../process/feign/BusinessProcessClient.java | 2 +- .../service/IBusinessProcessService.java | 2 +- .../impl/BusinessProcessServiceImpl.java | 9 +- doc/sql/changelog/process-202608071800.sql | 31 + 30 files changed, 1930 insertions(+), 173 deletions(-) create mode 100644 blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/constant/ProcessLockKeyConstant.java delete mode 100644 blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ApiMKProcessFinishDTO.java delete mode 100644 blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ProcessApprovalDTO.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/AsyncExecutorProperties.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/CurrentHandlerRefreshProperties.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/AbstractProcessOperationHandler.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessHandler.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationContext.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationHandler.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/AsyncService.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshService.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshTask.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/TaskState.java create mode 100644 blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/util/ProcessTypeUtils.java create mode 100644 blade-service/blade-openapi/src/main/resources/application.yml delete mode 100644 blade-service/blade-openapi/src/main/resources/bootstrap-dev.yml delete mode 100644 blade-service/blade-openapi/src/main/resources/bootstrap-prod.yml delete mode 100644 blade-service/blade-openapi/src/main/resources/bootstrap-test.yml delete mode 100644 blade-service/blade-openapi/src/main/resources/bootstrap.yml create mode 100644 doc/sql/changelog/process-202608071800.sql diff --git a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/api/IApi4MK.java b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/api/IApi4MK.java index 6c8d09d..701128a 100644 --- a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/api/IApi4MK.java +++ b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/api/IApi4MK.java @@ -14,9 +14,6 @@ public interface IApi4MK { String API_PREFIX = "/openApi/mk"; String PROCESS_API_PREFIX = API_PREFIX + "/process"; String PROCESS_FINISH_CALLBACK = PROCESS_API_PREFIX + "/finishCallback"; - String PROCESS_APPROVAL_CALLBACK = PROCESS_API_PREFIX + "/approvalCallback"; - String PROCESS_REJECT_CALLBACK = PROCESS_API_PREFIX + "/rejectCallback"; - String PROCESS_REVOKE_CALLBACK = PROCESS_API_PREFIX + "/revokeCallback"; String PROCESS_COMMON_CALLBACK = PROCESS_API_PREFIX + "/commonCallback"; /** @@ -34,28 +31,4 @@ public interface IApi4MK { */ @PostMapping(PROCESS_FINISH_CALLBACK) FR processFinishCallback(@RequestBody Api4MKProcessApprovalDTO param); - - /** - * 流程审批同意回调接口 - * @param param - * @return - */ - @PostMapping(PROCESS_APPROVAL_CALLBACK) - FR processApprovalCallback(@RequestBody Api4MKProcessApprovalDTO param); - - /** - * 流程审批拒绝回调接口 - * @param param - * @return - */ - @PostMapping(PROCESS_REJECT_CALLBACK) - FR processRejectCallback(@RequestBody Api4MKProcessApprovalDTO param); - - /** - * 流程撤销回调接口 - * @param param - * @return - */ - @PostMapping(PROCESS_REVOKE_CALLBACK) - FR processRevokeCallback(@RequestBody Api4MKProcessApprovalDTO param); } diff --git a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/constant/ProcessLockKeyConstant.java b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/constant/ProcessLockKeyConstant.java new file mode 100644 index 0000000..4fa3c13 --- /dev/null +++ b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/constant/ProcessLockKeyConstant.java @@ -0,0 +1,30 @@ +package org.springblade.openapi.mk.constant; + +/** + * 流程当前处理人相关redis锁key常量类 + * @author bfhuange + * @since 2026/4/9 + */ +public class ProcessLockKeyConstant { + + /** + * 任务缓存key前缀 + */ + public static final String TASK_KEY_PREFIX = "process:cur-handler:task:"; + /** + * 等待队列key + */ + public static final String WAITING_KEY = "process:cur-handler:waiting"; + /** + * 流程锁key前缀 + */ + public static final String PROCESS_LOCK_KEY_PREFIX = "process:cur-handler:lock:"; + /** + * 派工锁key + */ + public static final String DISPATCH_LOCK_KEY = "process:cur-handler:dispatch-lock"; + /** + * worker租约map key + */ + public static final String WORKER_LEASE_KEY = "process:cur-handler:worker-leases"; +} diff --git a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/Api4MKProcessApprovalDTO.java b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/Api4MKProcessApprovalDTO.java index b71f71e..0941684 100644 --- a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/Api4MKProcessApprovalDTO.java +++ b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/Api4MKProcessApprovalDTO.java @@ -69,13 +69,4 @@ public class Api4MKProcessApprovalDTO implements Serializable { */ private String operatorLoginName; - //====================非mk回调参数,回调接口设置参数=================== - /** - * 是否流程已完成,非mk回调参数,回调接口设置参数 - */ - private boolean complete; - /** - * 审批状态,非mk回调参数,回调接口设置参数 - */ - private String approveStatus; } diff --git a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ApiMKProcessFinishDTO.java b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ApiMKProcessFinishDTO.java deleted file mode 100644 index 2e2b57f..0000000 --- a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ApiMKProcessFinishDTO.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.springblade.openapi.mk.pojo.dto; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -import java.io.Serial; -import java.io.Serializable; - -/** - * mk审批结束回调参数 - * @author bfhuange - * @date 2024/9/5 - */ -@Data -public class ApiMKProcessFinishDTO implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - /** - * 流程实例id - */ - private String processInstanceId; - /** - * 表单实例id - */ - private String formInstanceId; - /** - * 模板编码,template_拼接 ProcessTypeEnum 的值 - */ - private String templateCode; - /** - * 流程状态 - */ - private String processStatus; -} diff --git a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ProcessApprovalDTO.java b/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ProcessApprovalDTO.java deleted file mode 100644 index 2bed1af..0000000 --- a/blade-service-api/blade-open-api/src/main/java/org/springblade/openapi/mk/pojo/dto/ProcessApprovalDTO.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.springblade.openapi.mk.pojo.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -@Builder -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ProcessApprovalDTO implements Serializable { - - /** - * 流程实例id - */ - @Schema(description = "流程实例id") - protected String flowInstId; - - - /** - * 表单实例id - */ - protected String formInstanceId; - - /** - * 审批状态 - */ - protected String approveStatus; - - /** - * 下级审批人 - */ - protected String nextApproveUser; - - -} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java index 9218fe8..16cc18e 100644 --- a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java @@ -49,10 +49,10 @@ public interface IBusinessProcessClient { /** * 修改业务流程状态 * @param param - * @return + * @return 审批状态 */ @PostMapping(UPDATE_BUSINESS_PROCESS_STATUS) - FR updateBusinessProcessStatus(@Validated @RequestBody BusinessProcessUpdateDTO param); + FR updateBusinessProcessStatus(@Validated @RequestBody BusinessProcessUpdateDTO param); /** * 修改业务流程审批人 diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java index 981167c..d3a3a8d 100644 --- a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessExecuteDTO.java @@ -2,7 +2,6 @@ package org.springblade.process.pojo.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import org.springblade.system.pojo.dto.AdditionOperationParameterDTO; import java.io.Serial; import java.io.Serializable; diff --git a/blade-service/blade-openapi/pom.xml b/blade-service/blade-openapi/pom.xml index b25a56b..e036291 100644 --- a/blade-service/blade-openapi/pom.xml +++ b/blade-service/blade-openapi/pom.xml @@ -42,6 +42,10 @@ org.springblade blade-mk-api + + org.springblade + blade-process-api + org.mapstruct diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java index 9815c2e..e76743f 100644 --- a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java @@ -43,6 +43,7 @@ import org.springframework.context.annotation.ComponentScan; public class OpenApiApplication { public static void main(String[] args) { + BladeApplication.disableNacosLaunchConfig(); BladeApplication.run(AppConstant.APPLICATION_OPENAPI_NAME, OpenApiApplication.class, args); } diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java new file mode 100644 index 0000000..0f43c46 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java @@ -0,0 +1,93 @@ +package org.springblade.openapi.mk; + +import com.alibaba.fastjson2.JSON; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.api.FR; +import org.springblade.openapi.mk.api.IApi4MK; +import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO; +import org.springblade.openapi.mk.pojo.enums.ProcessOperationType; +import org.springblade.openapi.mk.support.base.ProcessHandler; +import org.springblade.openapi.mk.util.ProcessTypeUtils; +import org.springblade.thirdparty.mk.config.MKProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 提供给mk的api实现类 + * @author bfhuange + * @date 2024/9/9 + */ +@Slf4j +@Hidden +@RestController +public class Api4MK implements IApi4MK { + private final MKProperties mkProperties; + private final Map handlerMap; + + public Api4MK(MKProperties mkProperties, ObjectProvider> handlersProvider) { + this.mkProperties = mkProperties; + handlerMap = handlersProvider.getIfAvailable(Collections::emptyList).stream() + .flatMap(handler -> handler.getProcessTypes().stream() + .collect(Collectors.toMap(Function.identity(), type -> handler, (a, b) -> { + throw new ServiceException("重复的流程类型处理器"); + })) + .entrySet() + .stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> { + throw new ServiceException("重复的流程类型处理器"); + })); + } + + @Override + public FR processCommonCallback(Api4MKProcessApprovalDTO param) { + log.info("mk流程通用回调 操作名称:{} 参数:{}", ProcessOperationType.getOperationName(param.getOperation()), JSON.toJSONString(param)); + callback(param, ProcessHandler::approve); + return FR.status(true); + } + + @Override + public FR processFinishCallback(Api4MKProcessApprovalDTO param) { + log.info("mk流程结束回调 参数:{}", JSON.toJSONString(param)); + // 手动设置操作类型,兼容历史接口 + param.setOperation(ProcessOperationType.PROCESS_FINISH); + callback(param, ProcessHandler::approve); + return FR.status(true); + } + + /** + * 获取处理器 + * @param processType + * @return + */ + private ProcessHandler getHandler(String processType) { + if (StringUtils.isBlank(processType)) { + return null; + } + return handlerMap.get(processType); + } + + /** + * 回调处理 + * @param param + * @param consumer + */ + private void callback(Api4MKProcessApprovalDTO param, BiConsumer consumer) { + String processType = ProcessTypeUtils.getProcessType(param.getTemplateCode(), mkProperties.getTemplateCodePrefix()); + ProcessHandler handler = getHandler(processType); + if (handler != null) { + consumer.accept(handler, param); + return; + } + log.warn("未配置流程类型对应的处理器 流程类型:{}", processType); + } +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/AsyncExecutorProperties.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/AsyncExecutorProperties.java new file mode 100644 index 0000000..e85fe3f --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/AsyncExecutorProperties.java @@ -0,0 +1,27 @@ +package org.springblade.openapi.mk.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 当前处理人刷新相关异步线程池配置。 + * + * @author bfhuange + * @since 2026/4/9 + */ +@Data +@Component +@ConfigurationProperties(prefix = "async") +public class AsyncExecutorProperties { + + /** + * 当前处理人刷新工作线程池名称 + */ + private String workerExecutorName = "mkRefreshWorkerExecutor"; + + /** + * 当前处理人刷新调度线程池名称 + */ + private String schedulerExecutorName = "mkRefreshSchedulerExecutor"; +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/CurrentHandlerRefreshProperties.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/CurrentHandlerRefreshProperties.java new file mode 100644 index 0000000..741f8f7 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/config/CurrentHandlerRefreshProperties.java @@ -0,0 +1,52 @@ +package org.springblade.openapi.mk.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author bfhuange + * @since 2026/4/9 + */ +@Component +@ConfigurationProperties(prefix = "process.current-handler-refresh") +@Data +public class CurrentHandlerRefreshProperties { + /** + * 服务启动后的首次派工延迟,单位毫秒 + */ + private long startupDispatchDelayMillis = 3000L; + + /** + * 首次执行延迟,单位秒 + */ + private long initialDelaySeconds = 1; + /** + * 轮询间隔,单位秒 + */ + private long intervalSeconds = 1; + /** + * 最大重试次数 + */ + private int maxAttempts = 30; + /** + * 最大worker数 + */ + private int maxWorkers = 5; + /** + * worker租约秒数 + */ + private long workerLeaseSeconds =15; + /** + * 锁等待秒数 + */ + private long lockWaitSeconds = 1; + /** + * 完成任务TTL + */ + private long doneTtlMinutes = 5L; + /** + * 失败任务TTL + */ + private long failedTtlMinutes = 30L; +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/AbstractProcessOperationHandler.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/AbstractProcessOperationHandler.java new file mode 100644 index 0000000..f4f6668 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/AbstractProcessOperationHandler.java @@ -0,0 +1,301 @@ +package org.springblade.openapi.mk.support.base; + +import com.alibaba.fastjson2.JSON; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.api.FR; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO; +import org.springblade.openapi.mk.pojo.enums.ProcessCallbackType; +import org.springblade.openapi.mk.support.handler.ProcessCurrentHandlerRefreshService; +import org.springblade.openapi.mk.util.ProcessTypeUtils; +import org.springblade.process.feign.IBusinessProcessClient; +import org.springblade.process.pojo.dto.BusinessProcessUpdateDTO; +import org.springblade.process.pojo.enums.ApproveStatusEnum; +import org.springblade.process.pojo.vo.BusinessProcessVO; +import org.springblade.thirdparty.mk.config.MKProperties; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * 抽象流程操作处理器,实现公共逻辑 + * @author bfhuange + * @date 2024/9/9 + */ +@Slf4j +public abstract class AbstractProcessOperationHandler implements ProcessHandler, ProcessOperationHandler { + + @Autowired + protected IBusinessProcessClient processClient; + + @Autowired + protected ProcessCurrentHandlerRefreshService refreshService; + + @Autowired + protected MKProperties mkProperties; + + @Override + public List getProcessTypes() { + return List.of(this.getProcessType()); + } + + @Override + public void approve(Api4MKProcessApprovalDTO param) { + // 入口层只接收 MK 原始回调参数,随后统一组装为内部上下文对象, + // 把流程类型、审批状态、是否完成、是否异步等内部处理语义集中收口在这里。 + ProcessCallbackType callbackType = ProcessCallbackType.getCallbackType(param.getOperation()); + if (callbackType == null) { + log.error("未配置事件的操作:{}", param.getOperation()); + return; + } + switch (callbackType) { + // 提交 + case SUBMIT -> submit(buildSubmitContext(param)); + // 审批结束 + case FINISH -> approveFinish(buildFinishContext(param)); + // 撤回 + case RETRACT -> approveRevoke(buildRevokeContext(param)); + // 通过 + case PASS -> approvePass(buildPassContext(param)); + // 驳回 + case REJECT -> approveReject(buildRejectContext(param)); + // 废弃 + case ABANDON -> approveAbandon(buildAbandonContext(param)); + // 修改当前处理人 + case CHANGE_CUR_HANDLER -> handleCommon(buildChangeCurrentHandlerContext(param)); + default -> log.error("未配置事件的操作:{}", param.getOperation()); + } + } + + @Override + public void approveFinish(ProcessOperationContext param) { + approveCommon(param, this::approveFinishBusiness); + } + + @Override + public void approvePass(ProcessOperationContext param) { + approveCommon(param, this::approvePassBusiness); + } + + @Override + public void approveReject(ProcessOperationContext param) { + approveCommon(param, this::approveRejectBusiness); + } + + @Override + public void approveRevoke(ProcessOperationContext param) { + approveCommon(param, this::approveRevokeBusiness); + } + + /** + * 处理提交 + * @param param + */ + @Override + public void submit(ProcessOperationContext param) { + // 一般提交后只需要更新当前处理人 + handleCommon(param); + } + + /** + * 处理废弃 + * @param param + */ + @Override + public void approveAbandon(ProcessOperationContext param) { + approveCommon(param, this::approveAbandonBusiness); + } + + /** + * 处理审批通用逻辑 + * @param param + * @param businessHandler + */ + protected void approveCommon(ProcessOperationContext param, Consumer businessHandler) { + // 1. 更新流程状态 + updateBusinessProcessStatus(param); + // 2. 同步处理业务逻辑 + businessHandler.accept(param); + // 3. 处理当前处理人刷新 + handleCommon(param); + } + + /** + * 处理公共异步逻辑 + * @param param + */ + protected void handleCommon(ProcessOperationContext param) { + // 当前处理人支持按事件选择同步刷新或任务调度刷新 + if (param.isAsync()) { + refreshService.enqueue(param); + } else { + refreshService.refreshNow(param); + } + } + + /** + * 更新流程状态 + * @param param + */ + private void updateBusinessProcessStatus(ProcessOperationContext param) { + // 更新流程状态 + BusinessProcessUpdateDTO updateStatusParam = getBusinessProcessUpdateParam(param); + FR statusResult = processClient.updateBusinessProcessStatus(updateStatusParam); + if (FR.isNotSuccess(statusResult)) { + log.error("更新流程状态异常 :{}", JSON.toJSONString(statusResult)); + String errorMessage = Optional.ofNullable(statusResult) + .map(FR::getMsg) + .orElse(""); + throw new ServiceException("更新流程状态异常:" + errorMessage); + } + // 具体审批状态要以更新 BusinessProcess 返回的为准,有些比如驳回到上一个审批节点(非起草节点)的,不需要更新状态 + String approveStatus = statusResult.getData(); + if (StringUtil.isBlank(approveStatus)) { + param.setApproveStatus(null); + } else { + param.setApproveStatus(approveStatus); + } + } + + /** + * 获取流程更新参数 + * @param param + * @return + */ + private BusinessProcessUpdateDTO getBusinessProcessUpdateParam(ProcessOperationContext param) { + BusinessProcessUpdateDTO updateParam = new BusinessProcessUpdateDTO(); + updateParam.setProcessInstanceId(param.getProcessInstanceId()); + updateParam.setPromoterLoginName(param.getApplicantLoginName()); + updateParam.setOperationNodeId(param.getCurrentNodeId()); + updateParam.setOperationNodeNumber(param.getCurrentNodeNumber()); + updateParam.setComplete(param.isComplete()); + updateParam.setApproveStatus(param.getApproveStatus()); + updateParam.setRejectNodeId(param.getRejectNodeId()); + return updateParam; + } + + /** + * 构造提交流程上下文。 + */ + protected ProcessOperationContext buildSubmitContext(Api4MKProcessApprovalDTO callbackParam) { + return buildContext(callbackParam, ApproveStatusEnum.APPROVING.getValue(), false, true); + } + + /** + * 构造审批通过上下文。 + */ + protected ProcessOperationContext buildPassContext(Api4MKProcessApprovalDTO callbackParam) { + return buildContext(callbackParam, ApproveStatusEnum.APPROVING.getValue(), false, true); + } + + /** + * 构造流程结束上下文。 + */ + protected ProcessOperationContext buildFinishContext(Api4MKProcessApprovalDTO callbackParam) { + return buildContext(callbackParam, ApproveStatusEnum.APPROVED.getValue(), true, true); + } + + /** + * 构造驳回上下文。 + */ + protected ProcessOperationContext buildRejectContext(Api4MKProcessApprovalDTO callbackParam) { + return buildContext(callbackParam, ApproveStatusEnum.REJECTED.getValue(), false, true); + } + + /** + * 构造撤回上下文。 + */ + protected ProcessOperationContext buildRevokeContext(Api4MKProcessApprovalDTO callbackParam) { + return buildContext(callbackParam, ApproveStatusEnum.REVOCATION.getValue(), false, true); + } + + /** + * 构造废弃上下文。 + */ + protected ProcessOperationContext buildAbandonContext(Api4MKProcessApprovalDTO callbackParam) { + return buildContext(callbackParam, ApproveStatusEnum.ABANDON.getValue(), false, true); + } + + /** + * 构造仅刷新当前处理人的上下文。 + */ + protected ProcessOperationContext buildChangeCurrentHandlerContext(Api4MKProcessApprovalDTO callbackParam) { + return buildContext(callbackParam, null, false, true); + } + + /** + * 构造流程内部处理上下文。 + * 这里统一固化 processType,避免后续业务处理和异步刷新阶段再次根据模板编码反推。 + */ + protected ProcessOperationContext buildContext(Api4MKProcessApprovalDTO callbackParam, + String approveStatus, + boolean complete, + boolean async) { + return ProcessOperationContext.builder() + .callbackParam(callbackParam) + .processType(ProcessTypeUtils.getProcessType(callbackParam.getTemplateCode(), mkProperties.getTemplateCodePrefix())) + .approveStatus(approveStatus) + .complete(complete) + .async(async) + .build(); + } + + /** + * 获取流程类型 + * @return + */ + protected String getProcessType() { + throw new ServiceException("未配置流程类型"); + }; + + /** + * 当前处理人刷新成功后回调各业务模块 + * @param param 回调参数 + * @param businessProcessVO 最新流程快照 + */ + public void handleCurrentHandlerRefresh(ProcessOperationContext param, BusinessProcessVO businessProcessVO) { + if (businessProcessVO != null) { + this.commonBusiness(param, businessProcessVO); + } + } + + /** + * 处理公共业务逻辑 + * @param param + * @param businessProcessVO + */ + protected abstract void commonBusiness(ProcessOperationContext param, BusinessProcessVO businessProcessVO); + + /** + * 处理审批通过同步逻辑 + * @param param + */ + protected abstract void approvePassBusiness(ProcessOperationContext param); + + /** + * 处理流程结束同步逻辑 + * @param param + */ + protected abstract void approveFinishBusiness(ProcessOperationContext param); + + /** + * 处理审批驳回同步逻辑 + * @param param + */ + protected abstract void approveRejectBusiness(ProcessOperationContext param); + + /** + * 处理审批撤回同步逻辑 + * @param param + */ + protected abstract void approveRevokeBusiness(ProcessOperationContext param); + + /** + * 处理审批废弃同步逻辑 todo 为了避免代码报错,先用空实现 + * @param param + */ + protected void approveAbandonBusiness(ProcessOperationContext param) {}; +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessHandler.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessHandler.java new file mode 100644 index 0000000..f0b06d3 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessHandler.java @@ -0,0 +1,27 @@ +package org.springblade.openapi.mk.support.base; + + +import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO; + +import java.util.List; + +/** + * 流程处理器 + * @author bfhuange + * @date 2024/9/9 + */ +public interface ProcessHandler { + + /** + * 获取流程类型列表 + * @return + */ + List getProcessTypes(); + + /** + * 通用审批 + * @param param + */ + void approve(Api4MKProcessApprovalDTO param); + +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationContext.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationContext.java new file mode 100644 index 0000000..5bc9690 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationContext.java @@ -0,0 +1,139 @@ +package org.springblade.openapi.mk.support.base; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Builder; +import lombok.Data; +import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 流程回调内部处理上下文。 + *

+ * callbackParam 仅保留 MK 原始回调参数, + * 其余字段为 openapi 在处理过程中补充的上下文参数。 + *

+ *

+ * 设计目的: + * 1. 避免把内部推导字段继续堆到 MK 原始回调 DTO 上; + * 2. 对外保留原始回调对象,便于排查问题、记录日志和后续扩展; + * 3. 通过代理 getter 尽量兼容原来直接读取 DTO 字段的使用习惯,降低老流程和后续分支合并成本。 + *

+ * + * @author bfhuange + * @date 2026/4/9 + */ +@Data +@Builder +public class ProcessOperationContext implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * MK 原始回调参数 + */ + private Api4MKProcessApprovalDTO callbackParam; + + /** + * 流程类型 + */ + private String processType; + + /** + * 业务审批状态。 + * 这是系统内部按事件语义统一补充的状态,不属于 MK 原始回调参数。 + */ + private String approveStatus; + + /** + * 是否流程已完成。 + * 这是系统内部按事件语义统一补充的状态,不属于 MK 原始回调参数。 + */ + private boolean complete; + + /** + * 是否异步刷新当前处理人。 + * 用于控制当前处理人更新是走同步刷新还是异步调度任务。 + */ + private boolean async; + + @JsonIgnore + @JSONField(serialize = false) + public String getProcessInstanceId() { + return callbackParam == null ? null : callbackParam.getProcessInstanceId(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getFormInstanceId() { + return callbackParam == null ? null : callbackParam.getFormInstanceId(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getTemplateId() { + return callbackParam == null ? null : callbackParam.getTemplateId(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getTemplateCode() { + return callbackParam == null ? null : callbackParam.getTemplateCode(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getProcessStatus() { + return callbackParam == null ? null : callbackParam.getProcessStatus(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getApplicantLoginName() { + return callbackParam == null ? null : callbackParam.getApplicantLoginName(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getRejectNodeId() { + return callbackParam == null ? null : callbackParam.getRejectNodeId(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getCurrentNodeId() { + return callbackParam == null ? null : callbackParam.getCurrentNodeId(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getCurrentNodeNumber() { + return callbackParam == null ? null : callbackParam.getCurrentNodeNumber(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getOperation() { + return callbackParam == null ? null : callbackParam.getOperation(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getOperationName() { + return callbackParam == null ? null : callbackParam.getOperationName(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getApprovalOpinion() { + return callbackParam == null ? null : callbackParam.getApprovalOpinion(); + } + + @JsonIgnore + @JSONField(serialize = false) + public String getOperatorLoginName() { + return callbackParam == null ? null : callbackParam.getOperatorLoginName(); + } +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationHandler.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationHandler.java new file mode 100644 index 0000000..c914acd --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/base/ProcessOperationHandler.java @@ -0,0 +1,45 @@ +package org.springblade.openapi.mk.support.base; + +/** + * 流程操作处理器 + * @author bfhuange + * @since 2024/11/25 + */ +public interface ProcessOperationHandler { + + /** + * 提交 + * @param param + */ + void submit(ProcessOperationContext param); + + /** + * 审批结束 + * @param param + */ + void approveFinish(ProcessOperationContext param); + + /** + * 审批同意 + * @param param + */ + void approvePass(ProcessOperationContext param); + + /** + * 审批拒绝 + * @param param + */ + void approveReject(ProcessOperationContext param); + + /** + * 审批撤销 + * @param param + */ + void approveRevoke(ProcessOperationContext param); + + /** + * 审批废弃 + * @param param + */ + void approveAbandon(ProcessOperationContext param); +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/AsyncService.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/AsyncService.java new file mode 100644 index 0000000..87250c5 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/AsyncService.java @@ -0,0 +1,113 @@ +package org.springblade.openapi.mk.support.handler; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.dynamictp.core.DtpRegistry; +import org.dromara.dynamictp.core.aware.TaskEnhanceAware; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.openapi.mk.config.AsyncExecutorProperties; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @author bfhuange + * @date 2024/9/20 + */ +@Slf4j +@Service +public class AsyncService { + private final AsyncExecutorProperties asyncExecutorProperties; + private Executor workerExecutor; + private ScheduledExecutorService schedulerExecutor; + + public AsyncService(AsyncExecutorProperties asyncExecutorProperties) { + this.asyncExecutorProperties = asyncExecutorProperties; + } + + /** + * 启动完成后预加载并校验线程池配置,避免等到第一次真正执行任务时才发现线程池缺失或类型配置错误。 + */ + @Order(Ordered.HIGHEST_PRECEDENCE) + @EventListener(ApplicationReadyEvent.class) + public void initExecutors() { + this.workerExecutor = resolveWorkerExecutor(); + this.schedulerExecutor = resolveSchedulerExecutor(); + log.info("当前处理人刷新异步线程池初始化完成,workerExecutorName:{},schedulerExecutorName:{}", + asyncExecutorProperties.getWorkerExecutorName(), asyncExecutorProperties.getSchedulerExecutorName()); + } + + /** + * 立即异步执行 + * @param runnable 任务 + */ + public void execute(Runnable runnable) { + getWorkerExecutor().execute(runnable); + } + + /** + * 延迟执行指定毫秒数。 + *

+ * 这里改为使用 ScheduledDtpExecutor 做真正的定时调度, + * 避免再通过线程池线程 sleep 的方式占用工作线程,导致真正的业务任务迟迟无法启动。 + *

+ * + * @param delayMillis 延迟毫秒数 + * @param runnable 任务 + */ + public void delayExecute(long delayMillis, Runnable runnable) { + if (delayMillis <= 0) { + execute(runnable); + return; + } + Runnable dispatchRunnable = wrapWithConfiguredTaskWrappers( + asyncExecutorProperties.getSchedulerExecutorName(), + () -> execute(runnable) + ); + getSchedulerExecutor().schedule(dispatchRunnable, delayMillis, TimeUnit.MILLISECONDS); + } + + private Executor getWorkerExecutor() { + return workerExecutor != null ? workerExecutor : resolveWorkerExecutor(); + } + + private ScheduledExecutorService getSchedulerExecutor() { + return schedulerExecutor != null ? schedulerExecutor : resolveSchedulerExecutor(); + } + + private Executor resolveWorkerExecutor() { + return DtpRegistry.getExecutor(asyncExecutorProperties.getWorkerExecutorName()); + } + + private ScheduledExecutorService resolveSchedulerExecutor() { + String schedulerExecutorName = asyncExecutorProperties.getSchedulerExecutorName(); + Executor executor = DtpRegistry.getExecutor(schedulerExecutorName); + if (executor instanceof ScheduledExecutorService scheduledExecutorService) { + return scheduledExecutorService; + } + String message = "线程池未按 ScheduledExecutorService 注册,name: " + schedulerExecutorName; + log.error(message); + throw new ServiceException(message); + } + + /** + * 按线程池已配置的 task wrappers 手动包装任务。 + *

+ * 当前使用的 dynamic-tp 版本下,ScheduledDtpExecutor 对 taskWrapper 的透传存在缺口, + * 这里直接读取线程池上已生效的 wrappers,按框架默认增强链顺序主动包装一次, + * 这样既能复用现有配置,又避免手写 mdc 透传逻辑与框架实现产生偏差。 + *

+ */ + private Runnable wrapWithConfiguredTaskWrappers(String executorName, Runnable runnable) { + Executor executor = DtpRegistry.getExecutor(executorName); + if (executor instanceof TaskEnhanceAware taskEnhanceAware) { + return taskEnhanceAware.getEnhancedTask(runnable, taskEnhanceAware.getTaskWrappers()); + } + return runnable; + } +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshService.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshService.java new file mode 100644 index 0000000..057f541 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshService.java @@ -0,0 +1,903 @@ +package org.springblade.openapi.mk.support.handler; + +import cn.hutool.core.util.IdUtil; +import com.alibaba.fastjson2.JSON; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RLock; +import org.redisson.api.RMapCache; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.redis.lock.RedisLockClient; +import org.springblade.core.tool.api.FR; +import org.springblade.openapi.mk.config.CurrentHandlerRefreshProperties; +import org.springblade.openapi.mk.constant.ProcessLockKeyConstant; +import org.springblade.openapi.mk.pojo.enums.ProcessCallbackType; +import org.springblade.openapi.mk.support.base.AbstractProcessOperationHandler; +import org.springblade.openapi.mk.support.base.ProcessOperationContext; +import org.springblade.process.feign.IBusinessProcessClient; +import org.springblade.process.pojo.dto.BusinessProcessCurrentHandlerRefreshDTO; +import org.springblade.process.pojo.vo.BusinessProcessVO; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 当前处理人刷新调度服务。 + *

+ * 背景: + * 流程引擎回调业务系统时,流程往往还没有真正流转到下一个激活节点, + * 此时立即查询当前节点/当前处理人,拿到的仍可能是上一节点的旧结果。 + * 因此这里不再依赖一次性的固定延迟,而是改成“按流程实例维度入队 + 固定间隔轮询刷新”的调度模型。 + *

+ *

+ * 整体流程: + * 1. openapi 收到流程事件后,先同步更新业务流程状态; + * 2. 如果当前事件要求异步刷新当前处理人,则调用 {@link #enqueue(ProcessOperationContext)} 写入刷新任务; + * 3. 任务以流程实例 id 为唯一主记录保存在 Redis,记录期望版本、执行版本、基线快照、最近回调参数、重试次数等信息; + * 4. 同一个流程实例只保留一条主任务记录,新的回调不会重复创建任务,只会提升 {@code desiredVersion} 并覆盖最近一次回调参数; + * 5. 等待中的流程实例 id 会放入 Redis ZSet,score 为下次重试时间,用于按时间顺序派工; + * 6. 调度器 {@link #tryDispatch()} 会在集群范围内抢占派工锁,按配置的最大 worker 数拉起异步 worker; + * 7. worker 执行时调用 system 侧“只刷新当前节点/当前处理人”接口,并比较“当前节点 + 当前处理人”快照是否相对基线发生变化; + * 8. 如果快照未变化,说明流程大概率还没流转完成,则按固定间隔重新入队重试; + * 9. 如果快照发生变化,则回调对应业务处理器 {@link AbstractProcessOperationHandler#handleCurrentHandlerRefresh(ProcessOperationContext, BusinessProcessVO)}; + * 10. 若执行期间又收到同一流程的新回调,则旧版本执行完后会把最新快照提升为新基线,并重新排到队尾,避免同一流程长期占用 worker; + * 11. 当达到最大重试次数后,任务进入失败态并保留一段时间,便于排查; + * 12. 成功完成的任务进入完成态并短期保留,随后自动过期。 + *

+ *

+ * 集群与并发约束: + * 1. 流程实例级别使用分布式锁,保证同一流程实例的任务状态变更串行化; + * 2. 派工使用全局分布式锁,保证多个实例不会同时超发 worker; + * 3. 活跃 worker 数通过 Redis 租约控制,服务异常中断后,租约超时即可视为 worker 失活; + * 4. 运行中的任务会持续更新心跳,如果服务升级、中断或线程异常退出,超时恢复逻辑会把任务重新转回等待态; + * 5. 启动时不会全量恢复运行中任务,避免在集群环境中误伤其他实例上仍在执行的任务。 + *

+ *

+ * 成功判定规则: + * 不再区分终态/非终态,也不依赖回调里传入的 complete true/false 单独判定是否成功, + * 统一以“当前节点变化 + 当前处理人变化后的最新快照”是否相对基线发生变化作为刷新成功依据。 + *

+ * + * @author bfhuange + * @date 2026/4/9 + */ +@Slf4j +@Service +public class ProcessCurrentHandlerRefreshService { + + + private final AsyncService asyncService; + private final BladeRedis bladeRedis; + private final RedisLockClient redisLockClient; + private final IBusinessProcessClient processClient; + private final CurrentHandlerRefreshProperties refreshProperties; + private final ObjectProvider> handlersProvider; + private final Map handlerMap; + + public ProcessCurrentHandlerRefreshService(AsyncService asyncService, + BladeRedis bladeRedis, + RedisLockClient redisLockClient, + IBusinessProcessClient processClient, + CurrentHandlerRefreshProperties refreshProperties, + ObjectProvider> handlersProvider) { + this.asyncService = asyncService; + this.bladeRedis = bladeRedis; + this.redisLockClient = redisLockClient; + this.processClient = processClient; + this.refreshProperties = refreshProperties; + this.handlersProvider = handlersProvider; + this.handlerMap = new ConcurrentHashMap<>(); + } + + /** + * 服务启动后恢复未完成任务 + */ + @Order + @EventListener(ApplicationReadyEvent.class) + public void init() { + // 集群环境下不能在启动时无差别回收所有运行中任务,否则会误伤其他实例正在执行的任务 + asyncService.delayExecute(refreshProperties.getStartupDispatchDelayMillis(), this::tryDispatch); + } + + /** + * 按流程类型懒加载处理器,避免在bean初始化阶段提前拉起handler导致循环依赖 + */ + private AbstractProcessOperationHandler getHandler(String processType) { + if (StringUtils.isBlank(processType)) { + return null; + } + if (handlerMap.isEmpty()) { + synchronized (this) { + if (handlerMap.isEmpty()) { + List handlers = handlersProvider.getIfAvailable(Collections::emptyList); + handlers.forEach(handler -> handler.getProcessTypes() + .forEach(type -> this.handlerMap.put(type, handler))); + } + } + } + return handlerMap.get(processType); + } + + /** + * 写入刷新任务 + * + * @param param 回调参数 + */ + public void enqueue(ProcessOperationContext param) { + if (param == null || StringUtils.isAnyBlank(param.getProcessType(), param.getProcessInstanceId())) { + log.warn("当前处理人刷新任务入队失败,上下文为空或流程类型/流程实例id为空,param:{}", JSON.toJSONString(param)); + return; + } + long now = System.currentTimeMillis(); + String processInstanceId = param.getProcessInstanceId(); + RLock lock = getProcessLock(processInstanceId); + boolean locked = false; + try { + locked = lock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!locked) { + log.warn("获取流程刷新任务锁失败,流程实例id:{}", processInstanceId); + return; + } + ProcessCurrentHandlerRefreshTask task = getTask(processInstanceId); + if (task == null) { + task = new ProcessCurrentHandlerRefreshTask(); + task.setProcessInstanceId(processInstanceId); + task.setState(TaskState.STATE_WAITING); + } + task.setProcessType(param.getProcessType()); + task.setContext(param); + task.setDesiredVersion(task.getDesiredVersion() + 1); + task.setLastCallbackAt(now); + if (!TaskState.STATE_RUNNING.equals(task.getState())) { + // 非运行中任务表示上一轮刷新周期已经结束或尚未开始。 + // 这里必须按本次回调重新建立基线,避免撤回后再次提交时沿用上一轮旧快照,导致新一轮刷新永远无法命中成功条件。 + task.setBaselineSnapshot(queryCurrentSnapshot(processInstanceId)); + task.setLatestSnapshot(null); + task.setLastSuccessAt(null); + task.setState(TaskState.STATE_WAITING); + task.setAttemptCount(0); + task.setProcessingVersion(0); + task.setRunToken(null); + task.setStartedAt(null); + task.setHeartbeatAt(null); + task.setNextRetryAt(now + initialDelayMillis()); + saveTask(task); + putWaitingTask(processInstanceId, task.getNextRetryAt()); + log.info("当前处理人刷新任务入队,流程实例id:{},{}", processInstanceId, formatTaskLog(task)); + scheduleDispatch(initialDelayMillis()); + } else { + saveTask(task); + log.info("当前处理人刷新任务更新执行中版本,流程实例id:{},{}", processInstanceId, formatTaskLog(task)); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("当前处理人刷新任务入队被中断,流程实例id:{}", processInstanceId, e); + } catch (Exception e) { + log.error("当前处理人刷新任务入队异常,流程实例id:{}", processInstanceId, e); + } finally { + unlock(lock); + } + tryDispatch(); + } + + /** + * 同步立即刷新当前处理人 + * + * @param param 回调参数 + */ + public void refreshNow(ProcessOperationContext param) { + if (param == null || StringUtils.isAnyBlank(param.getProcessType(), param.getProcessInstanceId())) { + log.warn("同步刷新当前处理人失败,上下文为空或流程类型/流程实例id为空,param:{}", JSON.toJSONString(param)); + return; + } + FR result = processClient.refreshBusinessProcessCurrentHandlers(buildUpdateParam(param)); + if (result == null || FR.isNotSuccess(result)) { + log.warn("同步刷新当前处理人失败,转入异步队列重试,流程实例id:{} result:{}", param.getProcessInstanceId(), JSON.toJSONString(result)); + enqueue(param); + return; + } + AbstractProcessOperationHandler handler = getHandler(param.getProcessType()); + if (handler == null) { + log.error("同步刷新当前处理人失败,未找到处理器,流程类型:{} 流程实例id:{}", param.getProcessType(), param.getProcessInstanceId()); + return; + } + handler.handleCurrentHandlerRefresh(param, result.getData()); + } + + /** + * 派发worker执行任务 + */ + public void tryDispatch() { + int activeWorkerCount = getActiveWorkerCount(); + if (activeWorkerCount >= refreshProperties.getMaxWorkers()) { + log.info("当前处理人刷新派工跳过,活跃worker已满,activeWorkers:{},maxWorkers:{}", activeWorkerCount, refreshProperties.getMaxWorkers()); + return; + } + RLock dispatchLock = getDispatchLock(); + boolean locked = false; + try { + locked = dispatchLock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!locked) { + return; + } + // 先恢复真正超时的运行中任务,再判断是否有可执行任务 + recoverTimeoutTasks(); + if (!hasWaitingTask()) { + return; + } + activeWorkerCount = getActiveWorkerCount(); + if (activeWorkerCount >= refreshProperties.getMaxWorkers()) { + log.info("当前处理人刷新派工二次检查跳过,活跃worker已满,activeWorkers:{},maxWorkers:{}", activeWorkerCount, refreshProperties.getMaxWorkers()); + return; + } + while ((activeWorkerCount = getActiveWorkerCount()) < refreshProperties.getMaxWorkers()) { + ProcessCurrentHandlerRefreshTask task = claimNextRunnableTaskUnderDispatchLock(); + if (task == null) { + return; + } + String workerId = IdUtil.fastSimpleUUID(); + refreshWorkerLease(workerId); + log.info("当前处理人刷新任务派工成功,workerId:{},activeWorkers:{},maxWorkers:{},流程实例id:{},{}", + workerId, activeWorkerCount, refreshProperties.getMaxWorkers(), task.getProcessInstanceId(), formatTaskLog(task)); + // worker租约到期前再触发一次派工,用于兜底恢复异常中断任务 + scheduleDispatch(workerLeaseMillis()); + asyncService.execute(() -> workerLoop(workerId, task)); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("当前处理人刷新派工被中断", e); + } catch (Exception e) { + log.error("当前处理人刷新派工异常", e); + } finally { + unlock(dispatchLock); + } + } + + /** + * worker循环拉取任务,尽量复用已经占用的worker槽位 + * + * @param workerId worker id + * @param firstTask 第一条任务 + */ + private void workerLoop(String workerId, ProcessCurrentHandlerRefreshTask firstTask) { + try { + ProcessCurrentHandlerRefreshTask task = firstTask; + while (task != null) { + log.info("当前处理人刷新worker开始执行任务,workerId:{},流程实例id:{},{}", workerId, task.getProcessInstanceId(), formatTaskLog(task)); + processTask(workerId, task); + refreshWorkerLease(workerId); + task = claimNextRunnableTask(); + } + } catch (Exception e) { + log.error("当前处理人刷新worker执行异常,workerId:{}", workerId, e); + } finally { + removeWorkerLease(workerId); + log.info("当前处理人刷新worker结束,workerId:{}", workerId); + tryDispatch(); + } + } + + /** + * 执行单条刷新任务 + * + * @param workerId worker id + * @param task 任务 + */ + private void processTask(String workerId, ProcessCurrentHandlerRefreshTask task) { + String processInstanceId = task.getProcessInstanceId(); + String runToken = task.getRunToken(); + if (!isTaskTokenMatched(processInstanceId, runToken)) { + log.info("当前处理人刷新任务执行前token已失效,workerId:{},流程实例id:{},runToken:{}", workerId, processInstanceId, runToken); + return; + } + updateHeartbeat(processInstanceId, runToken); + long startAt = System.currentTimeMillis(); + log.info("当前处理人刷新任务开始查询,workerId:{},流程实例id:{},runToken:{},attemptCount:{},baselineSnapshot:{}", + workerId, processInstanceId, runToken, task.getAttemptCount(), task.getBaselineSnapshot()); + FR result = processClient.refreshBusinessProcessCurrentHandlers(buildUpdateParam(task.getContext())); + updateHeartbeat(processInstanceId, runToken); + if (!isTaskTokenMatched(processInstanceId, runToken)) { + log.info("当前处理人刷新任务查询后token已失效,workerId:{},流程实例id:{},runToken:{}", workerId, processInstanceId, runToken); + return; + } + if (result == null || FR.isNotSuccess(result)) { + log.error("刷新当前处理人失败,流程实例id:{} result:{}", processInstanceId, JSON.toJSONString(result)); + requeueAfterMiss(task, false); + return; + } + BusinessProcessVO businessProcessVO = result.getData(); + // 只有“提交”事件需要额外等待离开回调节点; + // 审批通过/会签等场景允许节点不变但处理人变化,不能套用同一条规则,否则会误判为一直未流转 + if (shouldWaitForNextNode(task.getContext(), businessProcessVO)) { + log.info("当前处理人刷新任务命中等待下一节点条件,workerId:{},流程实例id:{},耗时:{}ms,latestSnapshot:{}", + workerId, processInstanceId, System.currentTimeMillis() - startAt, buildSnapshot(businessProcessVO)); + requeueAfterMiss(task, false); + return; + } + String latestSnapshot = buildSnapshot(businessProcessVO); + if (StringUtils.equals(latestSnapshot, task.getBaselineSnapshot())) { + log.info("当前处理人刷新任务快照未变化,workerId:{},流程实例id:{},耗时:{}ms,baselineSnapshot:{},latestSnapshot:{}", + workerId, processInstanceId, System.currentTimeMillis() - startAt, task.getBaselineSnapshot(), latestSnapshot); + requeueAfterMiss(task, false); + return; + } + log.info("当前处理人刷新任务命中成功条件,workerId:{},流程实例id:{},耗时:{}ms,baselineSnapshot:{},latestSnapshot:{}", + workerId, processInstanceId, System.currentTimeMillis() - startAt, task.getBaselineSnapshot(), latestSnapshot); + handleRefreshSuccess(task, businessProcessVO, latestSnapshot); + refreshWorkerLease(workerId); + } + + /** + * 处理刷新成功 + * + * @param task 任务 + * @param businessProcessVO 最新流程快照 + * @param latestSnapshot 最新快照 + */ + private void handleRefreshSuccess(ProcessCurrentHandlerRefreshTask task, BusinessProcessVO businessProcessVO, String latestSnapshot) { + String processInstanceId = task.getProcessInstanceId(); + String runToken = task.getRunToken(); + AbstractProcessOperationHandler handler = getHandler(task.getProcessType()); + if (handler == null) { + log.error("未找到当前处理人刷新处理器,流程类型:{} 流程实例id:{}", task.getProcessType(), processInstanceId); + requeueAfterMiss(task, true); + return; + } + ProcessOperationContext callbackParam = task.getContext(); + try { + handler.handleCurrentHandlerRefresh(callbackParam, businessProcessVO); + } catch (Exception e) { + log.error("刷新当前处理人后执行业务回调异常,流程实例id:{}", processInstanceId, e); + requeueAfterMiss(task, true); + return; + } + + RLock lock = getProcessLock(processInstanceId); + boolean locked = false; + try { + locked = lock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!locked) { + log.warn("刷新成功后回写任务失败,未获取到流程锁,流程实例id:{}", processInstanceId); + scheduleDispatch(intervalMillis()); + return; + } + ProcessCurrentHandlerRefreshTask latestTask = getTask(processInstanceId); + if (latestTask == null || !StringUtils.equals(runToken, latestTask.getRunToken())) { + return; + } + long now = System.currentTimeMillis(); + latestTask.setLatestSnapshot(latestSnapshot); + latestTask.setLastSuccessAt(now); + latestTask.setStartedAt(null); + latestTask.setHeartbeatAt(null); + latestTask.setRunToken(null); + if (latestTask.getDesiredVersion() > task.getProcessingVersion()) { + // 有新版本到来时,把最新快照提升为新基线,并重新排队到后面,避免一个流程长期占用worker + latestTask.setBaselineSnapshot(latestSnapshot); + latestTask.setAttemptCount(0); + latestTask.setState(TaskState.STATE_WAITING); + latestTask.setNextRetryAt(now + intervalMillis()); + saveTask(latestTask); + putWaitingTask(processInstanceId, latestTask.getNextRetryAt()); + log.info("当前处理人刷新任务成功后发现新版本,重新排队,流程实例id:{},{}", processInstanceId, formatTaskLog(latestTask)); + scheduleDispatch(intervalMillis()); + return; + } + latestTask.setState(TaskState.STATE_DONE); + saveTask(latestTask, Duration.ofMinutes(refreshProperties.getDoneTtlMinutes())); + removeWaitingTask(processInstanceId); + log.info("当前处理人刷新任务执行完成,流程实例id:{},{}", processInstanceId, formatTaskLog(latestTask)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("刷新成功后回写任务被中断,流程实例id:{}", processInstanceId, e); + } finally { + unlock(lock); + } + } + + /** + * 未命中最新快照时重新排队 + * + * @param task 任务 + * @param resetAttempt 是否重置重试次数 + */ + private void requeueAfterMiss(ProcessCurrentHandlerRefreshTask task, boolean resetAttempt) { + String processInstanceId = task.getProcessInstanceId(); + String runToken = task.getRunToken(); + RLock lock = getProcessLock(processInstanceId); + boolean locked = false; + try { + locked = lock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!locked) { + log.warn("刷新任务重新排队失败,未获取到流程锁,流程实例id:{}", processInstanceId); + scheduleDispatch(intervalMillis()); + return; + } + ProcessCurrentHandlerRefreshTask latestTask = getTask(processInstanceId); + if (latestTask == null || !StringUtils.equals(runToken, latestTask.getRunToken())) { + return; + } + long now = System.currentTimeMillis(); + boolean hasNewVersion = latestTask.getDesiredVersion() > task.getProcessingVersion(); + latestTask.setRunToken(null); + latestTask.setStartedAt(null); + latestTask.setHeartbeatAt(null); + latestTask.setState(TaskState.STATE_WAITING); + latestTask.setNextRetryAt(now + intervalMillis()); + if (resetAttempt || hasNewVersion) { + latestTask.setAttemptCount(0); + } else { + latestTask.setAttemptCount(latestTask.getAttemptCount() + 1); + } + if (latestTask.getAttemptCount() >= refreshProperties.getMaxAttempts()) { + latestTask.setState(TaskState.STATE_FAILED); + saveTask(latestTask, Duration.ofMinutes(refreshProperties.getFailedTtlMinutes())); + removeWaitingTask(processInstanceId); + log.warn("当前处理人刷新任务达到最大重试次数,流程实例id:{},{}", processInstanceId, formatTaskLog(latestTask)); + return; + } + saveTask(latestTask); + putWaitingTask(processInstanceId, latestTask.getNextRetryAt()); + log.info("当前处理人刷新任务重新排队,流程实例id:{},resetAttempt:{},hasNewVersion:{},{}", + processInstanceId, resetAttempt, hasNewVersion, formatTaskLog(latestTask)); + scheduleDispatch(intervalMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("刷新任务重新排队被中断,流程实例id:{}", processInstanceId, e); + } finally { + unlock(lock); + } + } + + /** + * claim下一条可执行任务 + * + * @return 任务,不存在时返回null + */ + private ProcessCurrentHandlerRefreshTask claimNextRunnableTask() { + RLock dispatchLock = getDispatchLock(); + boolean locked = false; + try { + locked = dispatchLock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!locked) { + return null; + } + recoverTimeoutTasks(); + return claimNextRunnableTaskUnderDispatchLock(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("claim当前处理人刷新任务被中断", e); + return null; + } finally { + unlock(dispatchLock); + } + } + + /** + * 在已持有派工锁的前提下claim下一条可执行任务 + * + * @return 任务,不存在时返回null + */ + private ProcessCurrentHandlerRefreshTask claimNextRunnableTaskUnderDispatchLock() { + Set processIds = bladeRedis.getStringRedisTemplate().opsForZSet() + .rangeByScore(ProcessLockKeyConstant.WAITING_KEY, 0, System.currentTimeMillis(), 0, 1); + if (processIds == null || processIds.isEmpty()) { + return null; + } + String processInstanceId = processIds.iterator().next(); + RLock processLock = getProcessLock(processInstanceId); + boolean processLocked = false; + try { + processLocked = processLock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!processLocked) { + return null; + } + ProcessCurrentHandlerRefreshTask task = getTask(processInstanceId); + if (task == null) { + removeWaitingTask(processInstanceId); + return null; + } + if (!TaskState.STATE_WAITING.equals(task.getState())) { + removeWaitingTask(processInstanceId); + return null; + } + if (task.getNextRetryAt() > System.currentTimeMillis()) { + putWaitingTask(processInstanceId, task.getNextRetryAt()); + return null; + } + task.setState(TaskState.STATE_RUNNING); + task.setProcessingVersion(task.getDesiredVersion()); + task.setRunToken(IdUtil.fastSimpleUUID()); + task.setStartedAt(System.currentTimeMillis()); + task.setHeartbeatAt(task.getStartedAt()); + saveTask(task); + removeWaitingTask(processInstanceId); + log.info("当前处理人刷新任务claim成功,流程实例id:{},{}", processInstanceId, formatTaskLog(task)); + return task; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("claim当前处理人刷新任务被中断,流程实例id:{}", processInstanceId, e); + return null; + } finally { + if (processLocked) { + unlock(processLock); + } + } + } + + /** + * 恢复超时的运行中任务 + */ + private void recoverTimeoutTasks() { + Set taskKeys = bladeRedis.getStringRedisTemplate().keys(ProcessLockKeyConstant.TASK_KEY_PREFIX + "*"); + if (taskKeys == null || taskKeys.isEmpty()) { + return; + } + long now = System.currentTimeMillis(); + for (String taskKey : taskKeys) { + ProcessCurrentHandlerRefreshTask task = getTaskByKey(taskKey); + if (task == null || !TaskState.STATE_RUNNING.equals(task.getState())) { + continue; + } + Long heartbeatAt = task.getHeartbeatAt(); + if (heartbeatAt != null && now - heartbeatAt <= workerLeaseMillis()) { + continue; + } + String processInstanceId = task.getProcessInstanceId(); + RLock lock = getProcessLock(processInstanceId); + boolean locked = false; + try { + locked = lock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!locked) { + continue; + } + ProcessCurrentHandlerRefreshTask latestTask = getTask(processInstanceId); + if (latestTask == null || !TaskState.STATE_RUNNING.equals(latestTask.getState())) { + continue; + } + Long latestHeartbeatAt = latestTask.getHeartbeatAt(); + if (latestHeartbeatAt != null && now - latestHeartbeatAt <= workerLeaseMillis()) { + continue; + } + latestTask.setState(TaskState.STATE_WAITING); + latestTask.setRunToken(null); + latestTask.setStartedAt(null); + latestTask.setHeartbeatAt(null); + latestTask.setNextRetryAt(now); + saveTask(latestTask); + putWaitingTask(processInstanceId, now); + log.warn("恢复超时的当前处理人刷新任务,流程实例id:{},{}", processInstanceId, formatTaskLog(latestTask)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("恢复超时任务被中断,流程实例id:{}", processInstanceId, e); + return; + } finally { + unlock(lock); + } + } + } + + /** + * 查询业务流程当前快照 + * + * @param processInstanceId 流程实例id + * @return 快照字符串 + */ + private String queryCurrentSnapshot(String processInstanceId) { + FR result = processClient.queryBusinessProcessSnapshot(processInstanceId); + if (result == null || FR.isNotSuccess(result)) { + log.warn("查询业务流程当前快照失败,流程实例id:{} result:{}", processInstanceId, JSON.toJSONString(result)); + return buildSnapshot(null); + } + return buildSnapshot(result.getData()); + } + + /** + * 构造刷新请求参数 + * + * @param callbackParam 回调参数 + * @return 刷新参数 + */ + private BusinessProcessCurrentHandlerRefreshDTO buildUpdateParam(ProcessOperationContext callbackParam) { + BusinessProcessCurrentHandlerRefreshDTO updateParam = new BusinessProcessCurrentHandlerRefreshDTO(); + updateParam.setProcessInstanceId(callbackParam.getProcessInstanceId()); + updateParam.setPromoterLoginName(callbackParam.getApplicantLoginName()); + updateParam.setComplete(callbackParam.isComplete()); + return updateParam; + } + + /** + * 构造快照,统一用当前节点+当前处理人作为变更依据 + * + * @param businessProcessVO 业务流程快照 + * @return 快照字符串 + */ + private String buildSnapshot(BusinessProcessVO businessProcessVO) { + if (businessProcessVO == null) { + return "|"; + } + return normalizeCsv(businessProcessVO.getCurrentNodeIds()) + "|" + normalizeCsv(businessProcessVO.getCurrentHandlers()); + } + + /** + * 提交回调时,如果刷新后仍停留在本次回调节点,说明流程尚未真正流转到下一激活节点,需要继续等待。 + *

+ * 这里只针对提交事件生效,不能推广到审批通过/会签等场景: + * 会签节点在部分人审批完成后,当前节点可能仍然不变,但当前处理人已经发生变化, + * 此时应当允许按“快照变化”判定成功,而不是继续等待节点变化。 + *

+ * + * @param callbackParam 回调参数 + * @param businessProcessVO 最新流程快照 + * @return 是否继续等待下一节点 + */ + private boolean shouldWaitForNextNode(ProcessOperationContext callbackParam, BusinessProcessVO businessProcessVO) { + if (callbackParam == null || businessProcessVO == null) { + return false; + } + if (ProcessCallbackType.SUBMIT != ProcessCallbackType.getCallbackType(callbackParam.getOperation())) { + return false; + } + String callbackNodeId = callbackParam.getCurrentNodeId(); + if (StringUtils.isBlank(callbackNodeId)) { + return false; + } + return containsCsvValue(businessProcessVO.getCurrentNodeIds(), callbackNodeId); + } + + /** + * 统一规范逗号拼接字段,避免比较时顺序影响结果 + * + * @param value 原始值 + * @return 规范化后的字符串 + */ + private String normalizeCsv(String value) { + if (StringUtils.isBlank(value)) { + return ""; + } + return Stream.of(value.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .distinct() + .sorted() + .collect(Collectors.joining(",")); + } + + /** + * 判断逗号分隔字段中是否包含指定值 + * + * @param csv 逗号分隔字段 + * @param target 目标值 + * @return 是否包含 + */ + private boolean containsCsvValue(String csv, String target) { + if (StringUtils.isAnyBlank(csv, target)) { + return false; + } + return Stream.of(csv.split(",")) + .map(String::trim) + .anyMatch(target::equals); + } + + /** + * 判断任务token是否仍然有效 + * + * @param processInstanceId 流程实例id + * @param runToken 运行token + * @return 是否匹配 + */ + private boolean isTaskTokenMatched(String processInstanceId, String runToken) { + ProcessCurrentHandlerRefreshTask latestTask = getTask(processInstanceId); + return latestTask != null + && TaskState.STATE_RUNNING.equals(latestTask.getState()) + && StringUtils.equals(runToken, latestTask.getRunToken()); + } + + /** + * 更新任务心跳,表示当前worker仍然存活 + * + * @param processInstanceId 流程实例id + * @param runToken 运行token + */ + private void updateHeartbeat(String processInstanceId, String runToken) { + RLock lock = getProcessLock(processInstanceId); + boolean locked = false; + try { + locked = lock.tryLock(refreshProperties.getLockWaitSeconds(), TimeUnit.SECONDS); + if (!locked) { + return; + } + ProcessCurrentHandlerRefreshTask task = getTask(processInstanceId); + if (task == null || !StringUtils.equals(runToken, task.getRunToken())) { + return; + } + task.setHeartbeatAt(System.currentTimeMillis()); + saveTask(task); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("更新刷新任务心跳被中断,流程实例id:{}", processInstanceId, e); + } finally { + unlock(lock); + } + } + + /** + * 按默认方式保存任务 + * + * @param task 任务 + */ + private void saveTask(ProcessCurrentHandlerRefreshTask task) { + bladeRedis.getStringRedisTemplate().opsForValue().set(getTaskKey(task.getProcessInstanceId()), JSON.toJSONString(task)); + } + + /** + * 按TTL保存任务 + * + * @param task 任务 + * @param ttl TTL + */ + private void saveTask(ProcessCurrentHandlerRefreshTask task, Duration ttl) { + bladeRedis.getStringRedisTemplate().opsForValue() + .set(getTaskKey(task.getProcessInstanceId()), JSON.toJSONString(task), ttl); + } + + /** + * 获取任务 + * + * @param processInstanceId 流程实例id + * @return 任务 + */ + private ProcessCurrentHandlerRefreshTask getTask(String processInstanceId) { + return getTaskByKey(getTaskKey(processInstanceId)); + } + + /** + * 通过key读取任务 + * + * @param taskKey 任务key + * @return 任务 + */ + private ProcessCurrentHandlerRefreshTask getTaskByKey(String taskKey) { + String content = bladeRedis.getStringRedisTemplate().opsForValue().get(taskKey); + if (StringUtils.isBlank(content)) { + return null; + } + return JSON.parseObject(content, ProcessCurrentHandlerRefreshTask.class); + } + + /** + * 放入等待队列 + * + * @param processInstanceId 流程实例id + * @param nextRetryAt 下次执行时间 + */ + private void putWaitingTask(String processInstanceId, long nextRetryAt) { + bladeRedis.getStringRedisTemplate().opsForZSet().add(ProcessLockKeyConstant.WAITING_KEY, processInstanceId, nextRetryAt); + } + + /** + * 移除等待队列中的任务 + * + * @param processInstanceId 流程实例id + */ + private void removeWaitingTask(String processInstanceId) { + bladeRedis.getStringRedisTemplate().opsForZSet().remove(ProcessLockKeyConstant.WAITING_KEY, processInstanceId); + } + + /** + * 是否存在等待任务 + * + * @return 是否存在 + */ + private boolean hasWaitingTask() { + Long size = bladeRedis.getStringRedisTemplate().opsForZSet().zCard(ProcessLockKeyConstant.WAITING_KEY); + return size != null && size > 0; + } + + /** + * 获取worker租约map + * + * @return worker租约map + */ + private RMapCache getWorkerLeaseMap() { + return redisLockClient.getRedissonClient().getMapCache(ProcessLockKeyConstant.WORKER_LEASE_KEY); + } + + /** + * 获取当前活跃worker数量 + * + * @return 活跃worker数量 + */ + private int getActiveWorkerCount() { + return getWorkerLeaseMap().size(); + } + + /** + * 刷新worker租约 + * + * @param workerId worker id + */ + private void refreshWorkerLease(String workerId) { + getWorkerLeaseMap().put(workerId, workerId, refreshProperties.getWorkerLeaseSeconds(), TimeUnit.SECONDS); + } + + /** + * 删除worker租约 + * + * @param workerId worker id + */ + private void removeWorkerLease(String workerId) { + getWorkerLeaseMap().remove(workerId); + } + + /** + * 安排稍后再次派工 + * + * @param delayMillis 延迟毫秒数 + */ + private void scheduleDispatch(long delayMillis) { + asyncService.delayExecute(delayMillis, this::tryDispatch); + } + + private RLock getProcessLock(String processInstanceId) { + return redisLockClient.getRedissonClient().getLock(ProcessLockKeyConstant.PROCESS_LOCK_KEY_PREFIX + processInstanceId); + } + + private RLock getDispatchLock() { + return redisLockClient.getRedissonClient().getLock(ProcessLockKeyConstant.DISPATCH_LOCK_KEY); + } + + /** + * 释放锁 + * + * @param lock + */ + private void unlock(RLock lock) { + if (lock.isLocked() && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + + private String getTaskKey(String processInstanceId) { + return ProcessLockKeyConstant.TASK_KEY_PREFIX + processInstanceId; + } + + private long initialDelayMillis() { + return refreshProperties.getInitialDelaySeconds() * 1000L; + } + + private long intervalMillis() { + return refreshProperties.getIntervalSeconds() * 1000L; + } + + private long workerLeaseMillis() { + return refreshProperties.getWorkerLeaseSeconds() * 1000L; + } + + private String formatTaskLog(ProcessCurrentHandlerRefreshTask task) { + if (task == null) { + return "task=null"; + } + return "state=" + task.getState() + + ", desiredVersion=" + task.getDesiredVersion() + + ", processingVersion=" + task.getProcessingVersion() + + ", attemptCount=" + task.getAttemptCount() + + ", nextRetryAt=" + task.getNextRetryAt() + + ", startedAt=" + task.getStartedAt() + + ", heartbeatAt=" + task.getHeartbeatAt() + + ", lastCallbackAt=" + task.getLastCallbackAt() + + ", lastSuccessAt=" + task.getLastSuccessAt(); + } +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshTask.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshTask.java new file mode 100644 index 0000000..90b08d2 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/ProcessCurrentHandlerRefreshTask.java @@ -0,0 +1,80 @@ +package org.springblade.openapi.mk.support.handler; + +import lombok.Data; +import org.springblade.openapi.mk.support.base.ProcessOperationContext; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 当前处理人刷新任务 + * + * @author bfhuange + * @date 2026/4/9 + */ +@Data +public class ProcessCurrentHandlerRefreshTask implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 流程实例id + */ + private String processInstanceId; + /** + * 流程类型 + */ + private String processType; + /** + * 任务状态 + */ + private String state; + /** + * 期望处理版本 + */ + private long desiredVersion; + /** + * 当前执行版本 + */ + private long processingVersion; + /** + * 当前执行令牌 + */ + private String runToken; + /** + * 当前基线快照 + */ + private String baselineSnapshot; + /** + * 最近一次成功快照 + */ + private String latestSnapshot; + /** + * 重试次数 + */ + private int attemptCount; + /** + * 下次重试时间 + */ + private long nextRetryAt; + /** + * 开始执行时间 + */ + private Long startedAt; + /** + * 最近心跳时间 + */ + private Long heartbeatAt; + /** + * 最近成功时间 + */ + private Long lastSuccessAt; + /** + * 最近回调时间 + */ + private Long lastCallbackAt; + /** + * 最近一次回调参数 + */ + private ProcessOperationContext context; +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/TaskState.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/TaskState.java new file mode 100644 index 0000000..0b255df --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/support/handler/TaskState.java @@ -0,0 +1,25 @@ +package org.springblade.openapi.mk.support.handler; + +/** + * 当前处理人刷新任务状态常量类 + * @author bfhuange + * @since 2026/4/9 + */ +public class TaskState { + /** + * 等待执行 + */ + public static final String STATE_WAITING = "WAITING"; + /** + * 执行中 + */ + public static final String STATE_RUNNING = "RUNNING"; + /** + * 执行完成 + */ + public static final String STATE_DONE = "DONE"; + /** + * 执行失败 + */ + public static final String STATE_FAILED = "FAILED"; +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/util/ProcessTypeUtils.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/util/ProcessTypeUtils.java new file mode 100644 index 0000000..6eff965 --- /dev/null +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/util/ProcessTypeUtils.java @@ -0,0 +1,23 @@ +package org.springblade.openapi.mk.util; + +import org.springblade.core.tool.utils.StringUtil; + +/** + * @author bfhuange + * @since 2026/7/12 + */ +public class ProcessTypeUtils { + + /** + * 获取流程类型 + * @param templateCode + * @param templateCodePrefix + * @return + */ + public static String getProcessType(String templateCode, String templateCodePrefix) { + if (StringUtil.isBlank(templateCode)) { + return templateCode; + } + return templateCode.replace(templateCodePrefix, ""); + } +} diff --git a/blade-service/blade-openapi/src/main/resources/application.yml b/blade-service/blade-openapi/src/main/resources/application.yml new file mode 100644 index 0000000..1905b44 --- /dev/null +++ b/blade-service/blade-openapi/src/main/resources/application.yml @@ -0,0 +1,27 @@ +server: + port: 8108 + +spring: + application: + name: blade-openapi + config: + import: + - nacos:blade.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:third-party-api.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:blade-openapi-dynamictp.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + cloud: + nacos: + username: ${NACOS_USERNAME:nacos} + password: ${NACOS_PASSWORD:nacos} + server-addr: ${NACOS_HOST:127.0.0.1:8848} + discovery: + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + config: + file-extension: yaml + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + datasource: + url: ${blade.datasource.${spring.profiles.active}.url} + username: ${blade.datasource.${spring.profiles.active}.username} + password: ${blade.datasource.${spring.profiles.active}.password} diff --git a/blade-service/blade-openapi/src/main/resources/bootstrap-dev.yml b/blade-service/blade-openapi/src/main/resources/bootstrap-dev.yml deleted file mode 100644 index 303fc85..0000000 --- a/blade-service/blade-openapi/src/main/resources/bootstrap-dev.yml +++ /dev/null @@ -1,6 +0,0 @@ -#spring: -# cloud: -# nacos: -# username: nacos -# password: ${NACOS_PASSWORD:gr30wIs5%Hi7keQj} -# server-addr: ${NACOS_ADDR:10.38.16.127:8848} diff --git a/blade-service/blade-openapi/src/main/resources/bootstrap-prod.yml b/blade-service/blade-openapi/src/main/resources/bootstrap-prod.yml deleted file mode 100644 index 673f396..0000000 --- a/blade-service/blade-openapi/src/main/resources/bootstrap-prod.yml +++ /dev/null @@ -1,6 +0,0 @@ -#spring: -# cloud: -# nacos: -# username: nacos -# password: rWrMrVTWyf%ekjuw -# server-addr: ${NACOS_ADDR:192.168.0.242:8848} diff --git a/blade-service/blade-openapi/src/main/resources/bootstrap-test.yml b/blade-service/blade-openapi/src/main/resources/bootstrap-test.yml deleted file mode 100644 index d449d60..0000000 --- a/blade-service/blade-openapi/src/main/resources/bootstrap-test.yml +++ /dev/null @@ -1,8 +0,0 @@ -#server: -# port: 38108 -#spring: -# cloud: -# nacos: -# username: nacos -# password: gr30wIs5%Hi7keQj -# server-addr: ${NACOS_ADDR:10.38.16.127:8848} diff --git a/blade-service/blade-openapi/src/main/resources/bootstrap.yml b/blade-service/blade-openapi/src/main/resources/bootstrap.yml deleted file mode 100644 index 41258cc..0000000 --- a/blade-service/blade-openapi/src/main/resources/bootstrap.yml +++ /dev/null @@ -1,34 +0,0 @@ -server: - port: 8108 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.dev.url} - username: ${blade.datasource.dev.username} - password: ${blade.datasource.dev.password} - -#spring: -# application: -# name: blade-openapi -# cloud: -# nacos: -# discovery: -# namespace: ${spring.profiles.active} -# # 不注册到nacos -# #registerEnabled: false -# config: -# # 文件后缀名 -# file-extension: yaml -# namespace: ${spring.profiles.active} -# shared-configs: -# - data-id: blade.yaml -# refresh: true -# - data-id: blade-${spring.profiles.active}.yaml -# refresh: true -# - data-id: third-party-api.yaml -# refresh: true -# extension-configs: -# - data-id: ${spring.application.name}-dynamictp.yaml -# group: DEFAULT_GROUP -# refresh: true # 必须配置,负责自动刷新不生效 diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java index e8ca2df..eb789f4 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java @@ -40,7 +40,7 @@ public class BusinessProcessClient implements IBusinessProcessClient { } @Override - public FR updateBusinessProcessStatus(@Validated @RequestBody BusinessProcessUpdateDTO param) { + public FR updateBusinessProcessStatus(@Validated @RequestBody BusinessProcessUpdateDTO param) { return FR.data(businessProcessService.updateBusinessProcessStatus(param)); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java index 9dd744d..5e3e2ff 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java @@ -30,7 +30,7 @@ public interface IBusinessProcessService extends IService { * @param param * @return */ - boolean updateBusinessProcessStatus(BusinessProcessUpdateDTO param); + String updateBusinessProcessStatus(BusinessProcessUpdateDTO param); /** * 修改业务流程审批人 diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java index 105651e..50984d3 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java @@ -116,7 +116,7 @@ public class BusinessProcessServiceImpl extends ServiceImpllambdaQuery() .eq(BusinessProcess::getProcessInstanceId, param.getProcessInstanceId()) @@ -128,9 +128,10 @@ public class BusinessProcessServiceImpl extends ServiceImpl Date: Mon, 10 Aug 2026 07:33:59 +0800 Subject: [PATCH 07/12] =?UTF-8?q?chore(deps):=20=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E6=8E=89=E6=8A=A5=E9=94=99=E7=9A=84=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-ops/blade-admin/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blade-ops/blade-admin/pom.xml b/blade-ops/blade-admin/pom.xml index 76408f9..321119f 100644 --- a/blade-ops/blade-admin/pom.xml +++ b/blade-ops/blade-admin/pom.xml @@ -83,11 +83,11 @@ spring-security-oauth2-autoconfigure
--> - + From 0aae137e7be8b19d0a90ce7308cac1b0597041ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Mon, 10 Aug 2026 07:34:35 +0800 Subject: [PATCH 08/12] =?UTF-8?q?feat(gateway):=20=E6=B7=BB=E5=8A=A0OAuth?= =?UTF-8?q?=20MK=E6=8E=A5=E5=8F=A3=E5=88=B0=E9=BB=98=E8=AE=A4=E8=B7=B3?= =?UTF-8?q?=E8=BF=87URL=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/springblade/gateway/provider/AuthProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java b/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java index 5bc679d..9520b05 100644 --- a/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java +++ b/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java @@ -52,6 +52,7 @@ public class AuthProvider { DEFAULT_SKIP_URL.add("/oauth/callback/**"); DEFAULT_SKIP_URL.add("/oauth/revoke/**"); DEFAULT_SKIP_URL.add("/oauth/refresh/**"); + DEFAULT_SKIP_URL.add("/oauth/mk/**"); DEFAULT_SKIP_URL.add("/token/**"); DEFAULT_SKIP_URL.add("/actuator/**"); DEFAULT_SKIP_URL.add("/v3/api-docs/**"); From 570aa77f4e453a761f86a4b3f3ce2800d4fad601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Mon, 10 Aug 2026 07:35:19 +0800 Subject: [PATCH 09/12] =?UTF-8?q?refactor(system):=20=E9=87=8D=E6=9E=84sys?= =?UTF-8?q?tem=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springblade/system/SystemApplication.java | 1 + .../src/main/resources/application-dev.yml | 10 ------- .../src/main/resources/application-prod.yml | 10 ------- .../src/main/resources/application-test.yml | 10 ------- .../src/main/resources/application.yml | 26 +++++++++++++++++++ 5 files changed, 27 insertions(+), 30 deletions(-) delete mode 100644 blade-service/blade-system/src/main/resources/application-dev.yml delete mode 100644 blade-service/blade-system/src/main/resources/application-prod.yml delete mode 100644 blade-service/blade-system/src/main/resources/application-test.yml create mode 100644 blade-service/blade-system/src/main/resources/application.yml diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java index b82811e..bbad903 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java @@ -39,6 +39,7 @@ import org.springframework.context.annotation.ComponentScan; public class SystemApplication { public static void main(String[] args) { + BladeApplication.disableNacosLaunchConfig(); BladeApplication.run(AppConstant.APPLICATION_SYSTEM_NAME, SystemApplication.class, args); } diff --git a/blade-service/blade-system/src/main/resources/application-dev.yml b/blade-service/blade-system/src/main/resources/application-dev.yml deleted file mode 100644 index 216bd19..0000000 --- a/blade-service/blade-system/src/main/resources/application-dev.yml +++ /dev/null @@ -1,10 +0,0 @@ -#服务器端口 -server: - port: 8106 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.dev.url} - username: ${blade.datasource.dev.username} - password: ${blade.datasource.dev.password} \ No newline at end of file diff --git a/blade-service/blade-system/src/main/resources/application-prod.yml b/blade-service/blade-system/src/main/resources/application-prod.yml deleted file mode 100644 index 25635bc..0000000 --- a/blade-service/blade-system/src/main/resources/application-prod.yml +++ /dev/null @@ -1,10 +0,0 @@ -#服务器端口 -server: - port: 8106 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.prod.url} - username: ${blade.datasource.prod.username} - password: ${blade.datasource.prod.password} diff --git a/blade-service/blade-system/src/main/resources/application-test.yml b/blade-service/blade-system/src/main/resources/application-test.yml deleted file mode 100644 index fb5cd8f..0000000 --- a/blade-service/blade-system/src/main/resources/application-test.yml +++ /dev/null @@ -1,10 +0,0 @@ -#服务器端口 -server: - port: 8106 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.test.url} - username: ${blade.datasource.test.username} - password: ${blade.datasource.test.password} diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml new file mode 100644 index 0000000..294659d --- /dev/null +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -0,0 +1,26 @@ +server: + port: 8106 + +spring: + application: + name: blade-system + config: + import: + - nacos:blade.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:third-party-api.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + cloud: + nacos: + username: ${NACOS_USERNAME:nacos} + password: ${NACOS_PASSWORD:nacos} + server-addr: ${NACOS_HOST:127.0.0.1:8848} + discovery: + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + config: + file-extension: yaml + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + datasource: + url: ${blade.datasource.${spring.profiles.active}.url} + username: ${blade.datasource.${spring.profiles.active}.username} + password: ${blade.datasource.${spring.profiles.active}.password} From e1fb293c57535018b5396ac4600f06c0ad8aa18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Mon, 10 Aug 2026 07:36:05 +0800 Subject: [PATCH 10/12] =?UTF-8?q?chore(deps):=20=E6=8E=92=E9=99=A4=20sprin?= =?UTF-8?q?g-cloud-starter-bootstrap=20=E4=BE=9D=E8=B5=96=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20nacos=20=E9=85=8D=E7=BD=AE=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=90=8E=E4=B8=8D=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/pom.xml | 4 ++++ blade-service/blade-openapi/pom.xml | 10 ++++++++++ blade-service/blade-system/pom.xml | 12 ++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/blade-auth/pom.xml b/blade-auth/pom.xml index 3799e81..b088727 100644 --- a/blade-auth/pom.xml +++ b/blade-auth/pom.xml @@ -25,6 +25,10 @@ org.springblade blade-scope-api + + spring-cloud-starter-bootstrap + org.springframework.cloud + diff --git a/blade-service/blade-openapi/pom.xml b/blade-service/blade-openapi/pom.xml index e036291..50180e1 100644 --- a/blade-service/blade-openapi/pom.xml +++ b/blade-service/blade-openapi/pom.xml @@ -46,6 +46,16 @@ org.springblade blade-process-api + + org.springblade + blade-core-launch + + + spring-cloud-starter-bootstrap + org.springframework.cloud + + + org.mapstruct diff --git a/blade-service/blade-system/pom.xml b/blade-service/blade-system/pom.xml index d3bde19..08aea8b 100644 --- a/blade-service/blade-system/pom.xml +++ b/blade-service/blade-system/pom.xml @@ -76,8 +76,16 @@ org.springblade blade-core-oauth2 - - + + org.springblade + blade-core-launch + + + spring-cloud-starter-bootstrap + org.springframework.cloud + + + From 8602262e169f03f113677eb26cc5aad226d763af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Mon, 10 Aug 2026 12:21:22 +0800 Subject: [PATCH 11/12] =?UTF-8?q?feat(process):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=AE=A1=E6=89=B9=E5=8F=82=E6=95=B0=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process/pojo/dto/ProcessApprovalDTO.java | 40 +++++++++++++ .../pojo/dto/ProcessNodeApprovalDTO.java | 57 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessApprovalDTO.java create mode 100644 blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessNodeApprovalDTO.java diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessApprovalDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessApprovalDTO.java new file mode 100644 index 0000000..70e3d01 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessApprovalDTO.java @@ -0,0 +1,40 @@ +package org.springblade.process.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProcessApprovalDTO implements Serializable { + + /** + * 流程实例id + */ + @Schema(description = "流程实例id") + protected String flowInstId; + + + /** + * 表单实例id + */ + protected String formInstanceId; + + /** + * 审批状态 + */ + protected String approveStatus; + + /** + * 下级审批人 + */ + protected String nextApproveUser; + + +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessNodeApprovalDTO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessNodeApprovalDTO.java new file mode 100644 index 0000000..96848c6 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/dto/ProcessNodeApprovalDTO.java @@ -0,0 +1,57 @@ +package org.springblade.process.pojo.dto; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * @author bfhuange + * @since 2025/3/10 + */ +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Data +public class ProcessNodeApprovalDTO extends ProcessApprovalDTO { + + /** + * 操作节点id,流程审批结束为空 + */ + private String operationNodeId; + + /** + * 操作节点编号,流程审批结束为空 + */ + private String operationNodeNumber; + + /** + * 操作人登录名 + */ + private String operatorLoginName; + + /** + * 流程模板id + */ + private String templateId; + + /** + * 驳回节点id N2 是起草节点 + */ + private String rejectNodeId; + + /** + * 是否流程已完成,非mk回调参数,回调接口设置参数 + */ + private boolean complete; + + @Builder(toBuilder = true, builderMethodName = "subBuilder", buildMethodName = "subBuild") + public ProcessNodeApprovalDTO(String flowInstId, String formInstanceId, String approveStatus, String nextApproveUser, String operationNodeId, String operationNodeNumber, String operatorLoginName, String templateId, String rejectNodeId, boolean complete) { + super(flowInstId, formInstanceId, approveStatus, nextApproveUser); + this.operationNodeId = operationNodeId; + this.operationNodeNumber = operationNodeNumber; + this.operatorLoginName = operatorLoginName; + this.templateId = templateId; + this.rejectNodeId = rejectNodeId; + this.complete = complete; + } +} From 4de1d35c2c35673780a3101f65de4142ef46fb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=96=8C=E5=B3=B0?= Date: Mon, 10 Aug 2026 13:40:16 +0800 Subject: [PATCH 12/12] =?UTF-8?q?refactor(file):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=9C=8D=E5=8A=A1=E9=85=8D=E7=BD=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/springblade/file/FileApplication.java | 1 + .../src/main/resources/application.yml | 26 ++++++++++++ .../src/main/resources/bootstrap-dev.yml | 6 --- .../src/main/resources/bootstrap-prod.yml | 6 --- .../src/main/resources/bootstrap-test.yml | 8 ---- .../src/main/resources/bootstrap.yml | 41 ------------------- 6 files changed, 27 insertions(+), 61 deletions(-) create mode 100644 blade-service/blade-file/src/main/resources/application.yml delete mode 100644 blade-service/blade-file/src/main/resources/bootstrap-dev.yml delete mode 100644 blade-service/blade-file/src/main/resources/bootstrap-prod.yml delete mode 100644 blade-service/blade-file/src/main/resources/bootstrap-test.yml delete mode 100644 blade-service/blade-file/src/main/resources/bootstrap.yml diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/FileApplication.java b/blade-service/blade-file/src/main/java/org/springblade/file/FileApplication.java index cfd985f..7870f66 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/FileApplication.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/FileApplication.java @@ -40,6 +40,7 @@ import org.springframework.context.annotation.ComponentScan; public class FileApplication { public static void main(String[] args) { + BladeApplication.disableNacosLaunchConfig(); BladeApplication.run(AppConstant.APPLICATION_FILE_NAME, FileApplication.class, args); } diff --git a/blade-service/blade-file/src/main/resources/application.yml b/blade-service/blade-file/src/main/resources/application.yml new file mode 100644 index 0000000..c63e919 --- /dev/null +++ b/blade-service/blade-file/src/main/resources/application.yml @@ -0,0 +1,26 @@ +server: + port: 8107 + +spring: + application: + name: blade-file + config: + import: + - nacos:blade.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - nacos:third-party-api.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + cloud: + nacos: + username: ${NACOS_USERNAME:nacos} + password: ${NACOS_PASSWORD:nacos} + server-addr: ${NACOS_HOST:127.0.0.1:8848} + discovery: + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + config: + file-extension: yaml + namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + datasource: + url: ${blade.datasource.${spring.profiles.active}.url} + username: ${blade.datasource.${spring.profiles.active}.username} + password: ${blade.datasource.${spring.profiles.active}.password} diff --git a/blade-service/blade-file/src/main/resources/bootstrap-dev.yml b/blade-service/blade-file/src/main/resources/bootstrap-dev.yml deleted file mode 100644 index 303fc85..0000000 --- a/blade-service/blade-file/src/main/resources/bootstrap-dev.yml +++ /dev/null @@ -1,6 +0,0 @@ -#spring: -# cloud: -# nacos: -# username: nacos -# password: ${NACOS_PASSWORD:gr30wIs5%Hi7keQj} -# server-addr: ${NACOS_ADDR:10.38.16.127:8848} diff --git a/blade-service/blade-file/src/main/resources/bootstrap-prod.yml b/blade-service/blade-file/src/main/resources/bootstrap-prod.yml deleted file mode 100644 index 673f396..0000000 --- a/blade-service/blade-file/src/main/resources/bootstrap-prod.yml +++ /dev/null @@ -1,6 +0,0 @@ -#spring: -# cloud: -# nacos: -# username: nacos -# password: rWrMrVTWyf%ekjuw -# server-addr: ${NACOS_ADDR:192.168.0.242:8848} diff --git a/blade-service/blade-file/src/main/resources/bootstrap-test.yml b/blade-service/blade-file/src/main/resources/bootstrap-test.yml deleted file mode 100644 index 7586f5a..0000000 --- a/blade-service/blade-file/src/main/resources/bootstrap-test.yml +++ /dev/null @@ -1,8 +0,0 @@ -#server: -# port: 38107 -#spring: -# cloud: -# nacos: -# username: nacos -# password: gr30wIs5%Hi7keQj -# server-addr: ${NACOS_ADDR:10.38.16.127:8848} diff --git a/blade-service/blade-file/src/main/resources/bootstrap.yml b/blade-service/blade-file/src/main/resources/bootstrap.yml deleted file mode 100644 index 3dcc197..0000000 --- a/blade-service/blade-file/src/main/resources/bootstrap.yml +++ /dev/null @@ -1,41 +0,0 @@ -server: - port: 8107 - -#数据源配置 -spring: - datasource: - url: ${blade.datasource.dev.url} - username: ${blade.datasource.dev.username} - password: ${blade.datasource.dev.password} - -#spring: -# application: -# name: blade-file -# cloud: -# nacos: -# discovery: -# namespace: ${spring.profiles.active} -# # 不注册到nacos -# #registerEnabled: false -# config: -# # 文件后缀名 -# file-extension: yaml -# namespace: ${spring.profiles.active} -# shared-configs: -# - data-id: blade.yaml -# refresh: true -# - data-id: blade-${spring.profiles.active}.yaml -# refresh: true -# - data-id: third-party-api.yaml -# refresh: true -# sentinel: -# datasource: -# flow: -# nacos: -# serverAddr: ${spring.cloud.nacos.server-addr} -# username: ${spring.cloud.nacos.username} -# password: ${spring.cloud.nacos.password} -# dataId: sentinel-${spring.application.name}.json -# groupId: DEFAULT_GROUP -# namespace: ${spring.profiles.active} -# ruleType: flow