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 001/114] =?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 002/114] =?UTF-8?q?feat(launcher):=20=E6=B7=BB=E5=8A=A0Nac?= =?UTF-8?q?os=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 003/114] =?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 004/114] =?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 005/114] =?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 006/114] =?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 007/114] =?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 008/114] =?UTF-8?q?feat(gateway):=20=E6=B7=BB=E5=8A=A0OAut?= =?UTF-8?q?h=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 009/114] =?UTF-8?q?refactor(system):=20=E9=87=8D=E6=9E=84s?= =?UTF-8?q?ystem=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 010/114] =?UTF-8?q?chore(deps):=20=E6=8E=92=E9=99=A4=20spr?= =?UTF-8?q?ing-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 011/114] =?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 012/114] =?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 From 323ce61136638790c0b2d723d8f6ef776923fe66 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 13 Aug 2026 17:54:35 +0800 Subject: [PATCH 013/114] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E5=9D=97bug=202=E3=80=81=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/entity/VoucherImage.java | 35 +++ .../transport/pojo/entity/Waybill.java | 3 + .../transport/pojo/vo/ProcessConfigVO.java | 3 + blade-service/blade-transport/pom.xml | 8 + .../config/VoucherImportRabbitConfig.java | 39 +++ .../transport/config/VoucherMinioConfig.java | 26 ++ .../controller/ProcessConfigController.java | 61 ++++- .../controller/VoucherManageController.java | 7 + .../controller/WaybillController.java | 17 +- .../event/VoucherUploadCompletedEvent.java | 24 ++ .../VoucherImportMessageListener.java | 26 ++ .../VoucherUploadCompletedListener.java | 29 +++ .../transport/mapper/VoucherImageMapper.java | 19 ++ .../mapper/VoucherWaybillBatchMapper.xml | 7 +- .../service/IVoucherManageService.java | 2 + .../transport/service/IWaybillService.java | 1 + .../impl/VoucherManageServiceImpl.java | 231 +++++++++++++++++- .../service/impl/WaybillServiceImpl.java | 21 ++ .../blade_voucher_image_20260813.sql | 23 ++ .../blade_waybill_route_json_20260813.sql | 2 + 20 files changed, 569 insertions(+), 15 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherImage.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/event/VoucherUploadCompletedEvent.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherImageMapper.java create mode 100644 doc/sql/transport/blade_voucher_image_20260813.sql create mode 100644 doc/sql/transport/blade_waybill_route_json_20260813.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherImage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherImage.java new file mode 100644 index 0000000..101fe2f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherImage.java @@ -0,0 +1,35 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** + * 凭证图片明细。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_voucher_image") +@Schema(description = "凭证图片明细") +public class VoucherImage extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + private Long voucherId; + private String voucherBatchNo; + private Long waybillId; + private String waybillNo; + private String plateNo; + private String imageName; + private String objectKey; + private Integer matched; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index 656e041..cbd2b07 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -229,6 +229,9 @@ public class Waybill extends TenantEntity { @Schema(description = "过程节点") private String processJson; + @Schema(description = "路线信息") + private String routeJson; + @Schema(description = "费用信息") private String freightJson; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java index 4b267a0..424a568 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java @@ -62,5 +62,8 @@ public class ProcessConfigVO extends ProcessConfig { @Schema(description = "状态名称") private String statusName; + @TableField(exist = false) + @Schema(description = "当前运单是否有关联凭证") + private Boolean hasRelatedVoucher; } diff --git a/blade-service/blade-transport/pom.xml b/blade-service/blade-transport/pom.xml index c8853b4..852de9a 100644 --- a/blade-service/blade-transport/pom.xml +++ b/blade-service/blade-transport/pom.xml @@ -39,6 +39,14 @@ org.springblade blade-system-api + + org.springframework.boot + spring-boot-starter-amqp + + + io.minio + minio + diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java new file mode 100644 index 0000000..e784e56 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.config; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 凭证导入消息队列配置。 + * RabbitMQ 连接参数由 Nacos 的 spring.rabbitmq 配置提供。 + */ +@Configuration +public class VoucherImportRabbitConfig { + + public static final String EXCHANGE = "tms.voucher.import.exchange"; + public static final String QUEUE = "tms.voucher.import.queue"; + public static final String ROUTING_KEY = "tms.voucher.import"; + + @Bean + public DirectExchange voucherImportExchange() { + return new DirectExchange(EXCHANGE, true, false); + } + + @Bean + public Queue voucherImportQueue() { + return new Queue(QUEUE, true); + } + + @Bean + public Binding voucherImportBinding(Queue voucherImportQueue, DirectExchange voucherImportExchange) { + return BindingBuilder.bind(voucherImportQueue).to(voucherImportExchange).with(ROUTING_KEY); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java new file mode 100644 index 0000000..5c0545b --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java @@ -0,0 +1,26 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.config; + +import io.minio.MinioClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 凭证图片 MinIO 客户端配置。 + * 连接参数由 Nacos 的 file.storage.minio 配置提供。 + */ +@Configuration +public class VoucherMinioConfig { + + @Bean + public MinioClient voucherMinioClient( + @Value("${file.storage.minio.endpoint:${minio.endpoint:}}") String endpoint, + @Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String accessKey, + @Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String secretKey) { + return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build(); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java index 12d2d3f..1bf3097 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java @@ -24,11 +24,13 @@ package org.springblade.transport.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.minio.GetPresignedObjectUrlArgs; +import io.minio.MinioClient; +import io.minio.http.Method; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; -import lombok.AllArgsConstructor; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.mp.support.Condition; @@ -38,10 +40,13 @@ import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.transport.excel.ProcessConfigExcel; +import org.springblade.transport.mapper.VoucherImageMapper; import org.springblade.transport.pojo.entity.ProcessConfig; +import org.springblade.transport.pojo.entity.VoucherImage; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.ProcessConfigVO; import org.springblade.transport.service.IProcessConfigService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -50,7 +55,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; /** * 过程配置 控制器 @@ -58,19 +66,64 @@ import java.util.List; * @author Chill */ @RestController -@AllArgsConstructor @PreAuth(menu = "process_config") @RequestMapping("/process-config") @Tag(name = "过程配置", description = "过程配置") public class ProcessConfigController extends BladeController { private final IProcessConfigService processConfigService; + private final VoucherImageMapper voucherImageMapper; + private final MinioClient minioClient; + @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") + private String minioBucketName; + + public ProcessConfigController(IProcessConfigService processConfigService, VoucherImageMapper voucherImageMapper, + MinioClient minioClient) { + this.processConfigService = processConfigService; + this.voucherImageMapper = voucherImageMapper; + this.minioClient = minioClient; + } @GetMapping("/detail") @ApiOperationSupport(order = 1) @Operation(summary = "详情", description = "传入id") - public R detail(@Parameter(description = "主键", required = true) @RequestParam Long id) { - return R.data(processConfigService.detail(id)); + public R detail(@Parameter(description = "主键", required = true) @RequestParam Long id, + @Parameter(description = "运单主键") @RequestParam(required = false) Long waybillId) { + ProcessConfigVO detail = processConfigService.detail(id); + detail.setHasRelatedVoucher(waybillId != null && voucherImageMapper.selectCount( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherImage::getWaybillId, waybillId) + .eq(VoucherImage::getMatched, 1)) > 0); + return R.data(detail); + } + + @GetMapping("/voucher-images") + @ApiOperationSupport(order = 2) + @Operation(summary = "查询运单已关联凭证图片") + public R>> voucherImages( + @Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) { + List> images = voucherImageMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherImage::getWaybillId, waybillId) + .eq(VoucherImage::getMatched, 1) + .orderByDesc(VoucherImage::getCreateTime)) + .stream().map(image -> { + Map result = new LinkedHashMap<>(); + result.put("id", image.getId()); + result.put("imageName", image.getImageName()); + result.put("plateNo", image.getPlateNo()); + result.put("waybillNo", image.getWaybillNo()); + result.put("objectKey", image.getObjectKey()); + try { + result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() + .method(Method.GET).bucket(minioBucketName).object(image.getObjectKey()) + .expiry(1, TimeUnit.HOURS).build())); + } catch (Exception e) { + throw new IllegalStateException("生成凭证图片预览地址失败", e); + } + return result; + }).toList(); + return R.data(images); } @GetMapping("/list") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java index b4d9ee7..dd98f3f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java @@ -65,6 +65,13 @@ public class VoucherManageController extends BladeController { return R.success("文件已更新"); } + @PostMapping("/reprocess") + @Operation(summary = "重新处理已上传凭证") + public R reprocess(@RequestParam Long id) { + voucherManageService.reprocessUploadedVoucher(id); + return R.success("重新处理完成"); + } + @PostMapping("/remove") @ApiOperationSupport(order = 5) @Operation(summary = "删除凭证") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index 53f83ad..3a749fe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -177,36 +177,43 @@ public class WaybillController extends BladeController { return R.data(waybillService.copy(id)); } - @PostMapping("/cancel") + @PostMapping("/change-route") @ApiOperationSupport(order = 11) + @Operation(summary = "变更运输路线", description = "传入运单路线与变更记录") + public R changeRoute(@RequestBody Waybill waybill) { + return R.status(waybillService.changeRoute(waybill)); + } + + @PostMapping("/cancel") + @ApiOperationSupport(order = 12) @Operation(summary = "取消", description = "传入id") public R cancel(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.cancel(id)); } @PostMapping("/reassign") - @ApiOperationSupport(order = 12) + @ApiOperationSupport(order = 13) @Operation(summary = "重新派单", description = "传入id") public R reassign(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.reassign(id)); } @PostMapping("/complete") - @ApiOperationSupport(order = 13) + @ApiOperationSupport(order = 14) @Operation(summary = "完成", description = "传入id") public R complete(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.complete(id)); } @PostMapping("/batch-complete") - @ApiOperationSupport(order = 14) + @ApiOperationSupport(order = 15) @Operation(summary = "批量完成", description = "传入ids") public R batchComplete(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.batchComplete(ids)); } @PostMapping("/road-loading") - @ApiOperationSupport(order = 15) + @ApiOperationSupport(order = 16) @Operation(summary = "公路配载", description = "传入ids") public R roadLoading(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.roadLoading(ids)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/event/VoucherUploadCompletedEvent.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/event/VoucherUploadCompletedEvent.java new file mode 100644 index 0000000..3b4cbdd --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/event/VoucherUploadCompletedEvent.java @@ -0,0 +1,24 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.event; + +import org.springframework.context.ApplicationEvent; + +/** + * 凭证压缩包上传完成事件。 + */ +public class VoucherUploadCompletedEvent extends ApplicationEvent { + + private final Long voucherId; + + public VoucherUploadCompletedEvent(Long voucherId) { + super(voucherId); + this.voucherId = voucherId; + } + + public Long getVoucherId() { + return voucherId; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java new file mode 100644 index 0000000..8b8f2bc --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java @@ -0,0 +1,26 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.listener; + +import lombok.RequiredArgsConstructor; +import org.springblade.transport.config.VoucherImportRabbitConfig; +import org.springblade.transport.service.IVoucherManageService; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +/** + * 凭证压缩包后台处理消费者。 + */ +@Component +@RequiredArgsConstructor +public class VoucherImportMessageListener { + + private final IVoucherManageService voucherManageService; + + @RabbitListener(queues = VoucherImportRabbitConfig.QUEUE) + public void processVoucher(Long voucherId) { + voucherManageService.processUploadedVoucher(voucherId); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java new file mode 100644 index 0000000..792af9d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java @@ -0,0 +1,29 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.listener; + +import lombok.RequiredArgsConstructor; +import org.springblade.transport.config.VoucherImportRabbitConfig; +import org.springblade.transport.event.VoucherUploadCompletedEvent; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * 凭证上传完成后投递后台处理消息。 + */ +@Component +@RequiredArgsConstructor +public class VoucherUploadCompletedListener { + + private final RabbitTemplate rabbitTemplate; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void publish(VoucherUploadCompletedEvent event) { + rabbitTemplate.convertAndSend(VoucherImportRabbitConfig.EXCHANGE, + VoucherImportRabbitConfig.ROUTING_KEY, event.getVoucherId()); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherImageMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherImageMapper.java new file mode 100644 index 0000000..2e8a516 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherImageMapper.java @@ -0,0 +1,19 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.springblade.transport.pojo.entity.VoucherImage; + +/** + * 凭证图片明细 Mapper。 + */ +public interface VoucherImageMapper extends BaseMapper { + + @Delete("DELETE FROM blade_voucher_image WHERE voucher_id = #{voucherId}") + void deleteByVoucherId(@Param("voucherId") Long voucherId); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml index 0c726f6..624250b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml @@ -2,9 +2,9 @@ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java index be5d296..ead0e8b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java @@ -17,6 +17,8 @@ public interface IVoucherManageService extends BaseService { void submit(VoucherManageSubmitRequest request); VoucherManage createUploadDraft(VoucherUploadDraftRequest request); void completeUploadFile(VoucherFileCompleteRequest request); + void processUploadedVoucher(Long voucherId); + void reprocessUploadedVoucher(Long voucherId); void removeVoucher(Long id); IPage> selectableWaybillBatches(IPage page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java index 56bbeec..c73eb58 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java @@ -46,6 +46,7 @@ public interface IWaybillService extends BaseService { List exportWaybill(WaybillVO waybill, String ids); List importWaybill(List data); WaybillVO copy(Long id); + boolean changeRoute(Waybill waybill); boolean cancel(Long id); boolean reassign(Long id); boolean complete(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java index 604dd91..0462940 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java @@ -6,38 +6,76 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.Func; import org.springblade.transport.mapper.VoucherManageMapper; +import org.springblade.transport.mapper.VoucherImageMapper; import org.springblade.transport.mapper.VoucherWaybillBatchMapper; +import org.springblade.transport.event.VoucherUploadCompletedEvent; import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest; import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest; import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest; import org.springblade.transport.pojo.entity.VoucherManage; import org.springblade.transport.pojo.entity.VoucherWaybillBatch; +import org.springblade.transport.pojo.entity.VoucherImage; +import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.vo.VoucherManageVO; import org.springblade.transport.service.IVoucherManageService; import org.springblade.transport.service.IProjectApplyService; +import org.springblade.transport.service.IWaybillService; import org.springblade.transport.wrapper.VoucherManageWrapper; import org.springframework.stereotype.Service; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; + import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; @Service +@Slf4j public class VoucherManageServiceImpl extends BaseServiceImpl implements IVoucherManageService { private final VoucherWaybillBatchMapper voucherWaybillBatchMapper; private final IProjectApplyService projectApplyService; + private final IWaybillService waybillService; + private final VoucherImageMapper voucherImageMapper; + private final ApplicationEventPublisher eventPublisher; + private final MinioClient minioClient; + private final String minioBucketName; + private final String minioRootDirectory; - public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService) { + public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService, + IWaybillService waybillService, + VoucherImageMapper voucherImageMapper, ApplicationEventPublisher eventPublisher, MinioClient minioClient, + @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName, + @Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) { this.voucherWaybillBatchMapper = voucherWaybillBatchMapper; this.projectApplyService = projectApplyService; + this.waybillService = waybillService; + this.voucherImageMapper = voucherImageMapper; + this.eventPublisher = eventPublisher; + this.minioClient = minioClient; + this.minioBucketName = minioBucketName; + this.minioRootDirectory = minioRootDirectory; } @Override @@ -114,10 +152,11 @@ public class VoucherManageServiceImpl extends BaseServiceImpl waybills = listRelatedWaybills(voucher); + Map waybillByPlate = new HashMap<>(); + for (Waybill waybill : waybills) { + waybillPlateNumbers(waybill).forEach(plateNo -> waybillByPlate.putIfAbsent(plateNo, waybill)); + } + voucherImageMapper.deleteByVoucherId(voucher.getId()); + int imageCount = 0; + Set relatedWaybillIds = new HashSet<>(); + try (InputStream source = openSourceFile(voucher.getFileUrl()); + ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), StandardCharsets.UTF_8)) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + String[] pathParts = entry.getName().replace('\\', '/').split("/"); + if (pathParts.length < 2 || !isImageFile(pathParts[pathParts.length - 1])) { + continue; + } + String plateNo = normalizePlateNo(pathParts[0]); + String imageName = safeFileName(pathParts[pathParts.length - 1]); + if (Func.isEmpty(plateNo) || Func.isEmpty(imageName)) { + continue; + } + Waybill waybill = waybillByPlate.get(plateNo); + String waybillNo = waybill == null ? "unmatched" : safePathPart(waybill.getWaybillNo()); + String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, imageName); + minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey) + .stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024).build()); + VoucherImage image = new VoucherImage(); + image.setVoucherId(voucher.getId()); + image.setVoucherBatchNo(voucher.getVoucherBatchNo()); + image.setWaybillId(waybill == null ? null : waybill.getId()); + image.setWaybillNo(waybill == null ? null : waybill.getWaybillNo()); + image.setPlateNo(plateNo); + image.setImageName(imageName); + image.setObjectKey(objectKey); + image.setMatched(waybill == null ? 0 : 1); + image.setTenantId(voucher.getTenantId()); + voucherImageMapper.insert(image); + imageCount++; + if (waybill != null) { + relatedWaybillIds.add(waybill.getId()); + } + } + } + VoucherManage update = new VoucherManage(); + update.setId(voucher.getId()); + update.setVoucherCount(imageCount); + update.setRelatedWaybillCount(relatedWaybillIds.size()); + update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0)); + update.setProcessStatus("处理完成"); + updateById(update); + } catch (Exception exception) { + VoucherManage update = new VoucherManage(); + update.setId(voucher.getId()); + update.setProcessStatus("处理失败"); + updateById(update); + log.error("凭证压缩包处理失败 voucherId:{}", voucherId, exception); + throw new ServiceException("凭证压缩包处理失败"); + } + } + @Override @Transactional(rollbackFor = Exception.class) public void removeVoucher(Long id) { @@ -155,6 +281,105 @@ public class VoucherManageServiceImpl extends BaseServiceImpl listRelatedWaybills(VoucherManage voucher) { + LambdaQueryWrapper query = Wrappers.lambdaQuery() + .eq(Waybill::getTenantId, voucher.getTenantId()) + .eq(Waybill::getIsDeleted, 0); + List batchNos = voucherWaybillBatchMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherWaybillBatch::getVoucherId, voucher.getId())) + .stream().map(VoucherWaybillBatch::getWaybillBatchNo).filter(Func::isNotEmpty).distinct().toList(); + if (Func.isEmpty(batchNos)) return List.of(); + List normalBatchNos = batchNos.stream().filter(batchNo -> !"未分批运单".equals(batchNo)).toList(); + boolean containsUnbatchedWaybills = batchNos.contains("未分批运单"); + query.and(wrapper -> { + if (Func.isNotEmpty(normalBatchNos)) { + wrapper.in(Waybill::getBatchNo, normalBatchNos); + } + if (containsUnbatchedWaybills) { + if (Func.isNotEmpty(normalBatchNos)) { + wrapper.or(); + } + wrapper.isNull(Waybill::getBatchNo).or().eq(Waybill::getBatchNo, ""); + } + }); + return waybillService.list(query); + } + + private InputStream openSourceFile(String fileUrl) throws Exception { + URLConnection connection = new java.net.URL(fileUrl).openConnection(); + connection.setConnectTimeout(30_000); + connection.setReadTimeout(300_000); + return connection.getInputStream(); + } + + private void validateMinioConfig() { + if (Func.isEmpty(minioBucketName)) { + throw new ServiceException("Nacos 未配置 file.storage.minio.bucket-name"); + } + } + + private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String imageName) { + String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + imageName; + if (Func.isEmpty(minioRootDirectory)) { + return objectKey; + } + return minioRootDirectory.endsWith("/") ? minioRootDirectory + objectKey : minioRootDirectory + "/" + objectKey; + } + + private String normalizePlateNo(String plateNo) { + return plateNo == null ? null : plateNo.replaceAll("[\\s-]", "").toUpperCase(); + } + + private Set waybillPlateNumbers(Waybill waybill) { + Set plateNumbers = new HashSet<>(); + addPlateNumber(plateNumbers, waybill.getVehicleNo()); + addPlateNumber(plateNumbers, waybill.getTrailerVehicleNo()); + addPlateNumbersFromJson(plateNumbers, waybill.getCarrierJson(), waybill.getId()); + addPlateNumbersFromJson(plateNumbers, waybill.getTaskInfoJson(), waybill.getId()); + return plateNumbers; + } + + private void addPlateNumbersFromJson(Set plateNumbers, String json, Long waybillId) { + if (Func.isEmpty(json)) return; + try { + Object parsed = JsonUtil.parse(json, Object.class); + if (parsed instanceof Map map) { + addPlateNumber(plateNumbers, String.valueOf(map.get("vehicleNo"))); + addPlateNumber(plateNumbers, String.valueOf(map.get("trailerVehicleNo"))); + } else if (parsed instanceof List rows) { + for (Object row : rows) { + if (row instanceof Map map) { + addPlateNumber(plateNumbers, String.valueOf(map.get("vehicleNo"))); + addPlateNumber(plateNumbers, String.valueOf(map.get("trailerVehicleNo"))); + } + } + } + } catch (Exception exception) { + log.warn("运单车牌信息解析失败 waybillId:{}", waybillId); + } + } + + private void addPlateNumber(Set plateNumbers, String plateNo) { + String normalizedPlateNo = normalizePlateNo(plateNo); + if (Func.isNotEmpty(normalizedPlateNo) && !"NULL".equals(normalizedPlateNo)) { + plateNumbers.add(normalizedPlateNo); + } + } + + private String safePathPart(String value) { + return value == null ? "" : value.replaceAll("[^0-9A-Za-z\\u4e00-\\u9fa5_-]", "_"); + } + + private String safeFileName(String value) { + return value == null ? "" : safePathPart(value.replaceFirst("(?s)^.*[/\\\\]", "")); + } + + private boolean isImageFile(String fileName) { + String lowerName = fileName.toLowerCase(); + return lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".png") + || lowerName.endsWith(".bmp") || lowerName.endsWith(".webp"); + } + private LambdaQueryWrapper buildQuery(VoucherManageVO query) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery().eq(VoucherManage::getIsDeleted, 0) .like(Func.isNotEmpty(query.getVoucherBatchNo()), VoucherManage::getVoucherBatchNo, query.getVoucherBatchNo()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 934c257..8d831c2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -224,6 +224,7 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setCarrierJson(source.getCarrierJson()); target.setTaskInfoJson(source.getTaskInfoJson()); target.setProcessJson(source.getProcessJson()); + target.setRouteJson(source.getRouteJson()); target.setFreightJson(source.getFreightJson()); target.setAttachmentsJson(source.getAttachmentsJson()); target.setRemark(source.getRemark()); @@ -235,6 +236,24 @@ public class WaybillServiceImpl extends BaseServiceImpl return detail(target.getId()); } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean changeRoute(Waybill waybill) { + Waybill oldRecord = loadEditable(waybill.getId(), true); + if ("completed".equals(oldRecord.getBusinessStatus()) || "cancelled".equals(oldRecord.getBusinessStatus())) { + throw new ServiceException("当前运单状态不允许变更运输路线"); + } + oldRecord.setRouteJson(TransportBusinessSupport.trimToNull(waybill.getRouteJson())); + oldRecord.setDepartureAddress(TransportBusinessSupport.trimToNull(waybill.getDepartureAddress())); + oldRecord.setArrivalAddress(TransportBusinessSupport.trimToNull(waybill.getArrivalAddress())); + oldRecord.setTaskInfoJson(TransportBusinessSupport.trimToNull(waybill.getTaskInfoJson())); + TransportBusinessSupport.validateLength(oldRecord.getRouteJson(), 8000, "路线信息不能超过8000字"); + TransportBusinessSupport.validateLength(oldRecord.getDepartureAddress(), 255, "发货地址不能超过255字"); + TransportBusinessSupport.validateLength(oldRecord.getArrivalAddress(), 255, "收货地址不能超过255字"); + TransportBusinessSupport.validateLength(oldRecord.getTaskInfoJson(), 8000, "任务信息不能超过8000字"); + return updateById(oldRecord); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean cancel(Long id) { @@ -470,6 +489,7 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setCarrierJson(TransportBusinessSupport.trimToNull(waybill.getCarrierJson())); waybill.setTaskInfoJson(TransportBusinessSupport.trimToNull(waybill.getTaskInfoJson())); waybill.setProcessJson(TransportBusinessSupport.trimToNull(waybill.getProcessJson())); + waybill.setRouteJson(TransportBusinessSupport.trimToNull(waybill.getRouteJson())); waybill.setFreightJson(TransportBusinessSupport.trimToNull(waybill.getFreightJson())); waybill.setAttachmentsJson(TransportBusinessSupport.trimToNull(waybill.getAttachmentsJson())); waybill.setDeptName(TransportBusinessSupport.trimToNull(waybill.getDeptName())); @@ -562,6 +582,7 @@ public class WaybillServiceImpl extends BaseServiceImpl TransportBusinessSupport.validateLength(waybill.getCarrierJson(), 8000, "承运信息不能超过8000字"); TransportBusinessSupport.validateLength(waybill.getTaskInfoJson(), 8000, "任务信息不能超过8000字"); TransportBusinessSupport.validateLength(waybill.getProcessJson(), 8000, "过程节点不能超过8000字"); + TransportBusinessSupport.validateLength(waybill.getRouteJson(), 8000, "路线信息不能超过8000字"); TransportBusinessSupport.validateLength(waybill.getFreightJson(), 8000, "费用信息不能超过8000字"); TransportBusinessSupport.validateLength(waybill.getAttachmentsJson(), 8000, "附件不能超过8000字"); TransportBusinessSupport.validateLength(waybill.getDeptName(), 255, "所属组织不能超过255字"); diff --git a/doc/sql/transport/blade_voucher_image_20260813.sql b/doc/sql/transport/blade_voucher_image_20260813.sql new file mode 100644 index 0000000..a43b095 --- /dev/null +++ b/doc/sql/transport/blade_voucher_image_20260813.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS `blade_voucher_image` ( + `id` bigint NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', + `voucher_id` bigint NOT NULL COMMENT '凭证批次ID', + `voucher_batch_no` varchar(30) NOT NULL COMMENT '凭证批次号', + `waybill_id` bigint DEFAULT NULL COMMENT '关联运单ID', + `waybill_no` varchar(64) DEFAULT NULL COMMENT '关联运单号', + `plate_no` varchar(32) NOT NULL COMMENT '车牌号', + `image_name` varchar(255) NOT NULL COMMENT '图片名称', + `object_key` varchar(1000) NOT NULL COMMENT 'MinIO对象路径', + `matched` tinyint NOT NULL DEFAULT 0 COMMENT '是否匹配运单:0否、1是', + `create_user` bigint DEFAULT NULL, + `create_dept` bigint DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` tinyint NOT NULL DEFAULT 1, + `is_deleted` tinyint NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_voucher_image_voucher` (`voucher_id`), + KEY `idx_voucher_image_waybill` (`waybill_id`), + KEY `idx_voucher_image_plate` (`tenant_id`, `plate_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证图片明细'; diff --git a/doc/sql/transport/blade_waybill_route_json_20260813.sql b/doc/sql/transport/blade_waybill_route_json_20260813.sql new file mode 100644 index 0000000..ac3e0d8 --- /dev/null +++ b/doc/sql/transport/blade_waybill_route_json_20260813.sql @@ -0,0 +1,2 @@ +ALTER TABLE `blade_waybill` + ADD COLUMN `route_json` text DEFAULT NULL COMMENT '路线信息' AFTER `process_json`; From 8e06aa3f684dd224bae6aad59b62b108f0897dbd Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 13 Aug 2026 20:58:52 +0800 Subject: [PATCH 014/114] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E5=9D=97bug=202=E3=80=81=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/vo/MasterOrderVO.java | 3 + .../config/VoucherImportRabbitConfig.java | 26 +++-- .../VoucherImportMessageListener.java | 23 ++++- .../VoucherUploadCompletedListener.java | 9 +- .../service/impl/MasterOrderServiceImpl.java | 94 ++++++++++++++----- .../impl/VoucherManageServiceImpl.java | 51 +++++++++- ...blade_waybill_non_road_fields_20260813.sql | 24 +++++ 7 files changed, 193 insertions(+), 37 deletions(-) create mode 100644 doc/sql/transport/blade_waybill_non_road_fields_20260813.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java index 76bda0c..490607a 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.MasterOrder; +import org.springblade.transport.pojo.entity.Waybill; import java.io.Serial; import java.math.BigDecimal; @@ -37,6 +38,8 @@ public class MasterOrderVO extends MasterOrder { @TableField(exist = false) private List> routeProgress; @TableField(exist = false) + private List boundWaybills; + @TableField(exist = false) private BigDecimal totalQuantity; @TableField(exist = false) private String createUserName; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java index e784e56..09fee09 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java @@ -10,6 +10,7 @@ import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; /** * 凭证导入消息队列配置。 @@ -18,22 +19,35 @@ import org.springframework.context.annotation.Configuration; @Configuration public class VoucherImportRabbitConfig { - public static final String EXCHANGE = "tms.voucher.import.exchange"; - public static final String QUEUE = "tms.voucher.import.queue"; - public static final String ROUTING_KEY = "tms.voucher.import"; + private final String exchange; + private final String queue; + private final String routingKey; + + public VoucherImportRabbitConfig( + @Value("${voucher.import.rabbit.exchange:tms.voucher.import.exchange}") String exchange, + @Value("${voucher.import.rabbit.queue:tms.voucher.import.queue}") String queue, + @Value("${voucher.import.rabbit.routing-key:tms.voucher.import}") String routingKey) { + this.exchange = exchange; + this.queue = queue; + this.routingKey = routingKey; + } + + public String getExchange() { return exchange; } + public String getQueue() { return queue; } + public String getRoutingKey() { return routingKey; } @Bean public DirectExchange voucherImportExchange() { - return new DirectExchange(EXCHANGE, true, false); + return new DirectExchange(exchange, true, false); } @Bean public Queue voucherImportQueue() { - return new Queue(QUEUE, true); + return new Queue(queue, true); } @Bean public Binding voucherImportBinding(Queue voucherImportQueue, DirectExchange voucherImportExchange) { - return BindingBuilder.bind(voucherImportQueue).to(voucherImportExchange).with(ROUTING_KEY); + return BindingBuilder.bind(voucherImportQueue).to(voucherImportExchange).with(routingKey); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java index 8b8f2bc..c97feb7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java @@ -5,22 +5,41 @@ package org.springblade.transport.listener; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springblade.transport.config.VoucherImportRabbitConfig; import org.springblade.transport.service.IVoucherManageService; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.stereotype.Component; +import org.springframework.context.event.EventListener; /** * 凭证压缩包后台处理消费者。 */ @Component @RequiredArgsConstructor +@Slf4j public class VoucherImportMessageListener { - private final IVoucherManageService voucherManageService; + private static final String LISTENER_ID = "voucherImportMessageListener"; - @RabbitListener(queues = VoucherImportRabbitConfig.QUEUE) + private final IVoucherManageService voucherManageService; + private final VoucherImportRabbitConfig voucherImportRabbitConfig; + private final RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry; + + @EventListener(ApplicationReadyEvent.class) + public void logConsumerStatus() { + MessageListenerContainer container = rabbitListenerEndpointRegistry.getListenerContainer(LISTENER_ID); + log.info("[凭证MQ] 消费者状态 listenerId={}, queue={}, registered={}, running={}", + LISTENER_ID, voucherImportRabbitConfig.getQueue(), container != null, container != null && container.isRunning()); + } + + @RabbitListener(id = LISTENER_ID, queues = "${voucher.import.rabbit.queue:tms.voucher.import.queue}") public void processVoucher(Long voucherId) { + log.info("[凭证MQ] 收到处理任务 queue={}, voucherId={}", voucherImportRabbitConfig.getQueue(), voucherId); voucherManageService.processUploadedVoucher(voucherId); + log.info("[凭证MQ] 处理任务完成 voucherId={}", voucherId); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java index 792af9d..7af86a9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java @@ -5,6 +5,7 @@ package org.springblade.transport.listener; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springblade.transport.config.VoucherImportRabbitConfig; import org.springblade.transport.event.VoucherUploadCompletedEvent; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -17,13 +18,17 @@ import org.springframework.transaction.event.TransactionalEventListener; */ @Component @RequiredArgsConstructor +@Slf4j public class VoucherUploadCompletedListener { private final RabbitTemplate rabbitTemplate; + private final VoucherImportRabbitConfig voucherImportRabbitConfig; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void publish(VoucherUploadCompletedEvent event) { - rabbitTemplate.convertAndSend(VoucherImportRabbitConfig.EXCHANGE, - VoucherImportRabbitConfig.ROUTING_KEY, event.getVoucherId()); + log.info("[凭证MQ] 投递处理任务 exchange={}, routingKey={}, voucherId={}", + voucherImportRabbitConfig.getExchange(), voucherImportRabbitConfig.getRoutingKey(), event.getVoucherId()); + rabbitTemplate.convertAndSend(voucherImportRabbitConfig.getExchange(), + voucherImportRabbitConfig.getRoutingKey(), event.getVoucherId()); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index f9a2bac..52663ce 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -78,7 +78,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl boundWaybills = waybillsByMasterNo(entity.getMasterNo()); + vo.setBoundWaybills(boundWaybills); + vo.setRouteProgress(buildProgress(entity, vo.getRoutes(), boundWaybills)); vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); return vo; } @@ -260,16 +263,18 @@ public class MasterOrderServiceImpl extends BaseServiceImpl> buildProgress(MasterOrder masterOrder, List> routes) { - List waybills = waybillService.list(new LambdaQueryWrapper().eq(Waybill::getMasterNo, masterOrder.getMasterNo())); + private List> buildProgress(MasterOrder masterOrder, List> routes, List waybills) { List plans = transportPlanService.list(new LambdaQueryWrapper().eq(TransportPlan::getMasterNo, masterOrder.getMasterNo())); for (Map route : routes) { String segmentNo = string(route, "segmentNo"); - route.put("dispatchedQuantity", dispatchedQuantity(masterOrder.getMasterNo(), segmentNo)); - route.put("dispatchedGoods", dispatchedGoods(masterOrder.getMasterNo(), segmentNo)); - route.put("arrivedQuantity", waybills.stream().filter(item -> Objects.equals(segmentNo, item.getRelationNo()) && "completed".equals(item.getBusinessStatus())).map(Waybill::getQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); - route.put("waybills", waybills.stream().filter(item -> Objects.equals(segmentNo, item.getRelationNo())).toList()); - route.put("transportPlans", plans.stream().filter(item -> Objects.equals(segmentNo, item.getRelationNo())).toList()); + List routeWaybills = waybills.stream().filter(item -> belongsToRoute(item, route)).toList(); + List routePlans = plans.stream().filter(item -> belongsToRoute(item, route)).toList(); + Map dispatchedGoods = dispatchedGoods(routeWaybills, routePlans); + route.put("dispatchedQuantity", dispatchedGoods.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add)); + route.put("dispatchedGoods", dispatchedGoods); + route.put("arrivedQuantity", routeWaybills.stream().filter(item -> "completed".equals(item.getBusinessStatus())).flatMap(item -> waybillGoods(item).stream()).map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add)); + route.put("waybills", routeWaybills); + route.put("transportPlans", routePlans); } return routes; } @@ -282,7 +287,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatch) { String carrierType = string(dispatch, "carrierType", "承运商"); + boolean road = string(dispatch, "transportType", "").toLowerCase().contains("road") || string(dispatch, "transportType", "").contains("公路"); + if (!road) { + if (Func.isEmpty(string(dispatch, "vehicleNo")) || Func.isEmpty(string(dispatch, "captainName")) || Func.isEmpty(string(dispatch, "driverPhone")) || Func.isEmpty(string(dispatch, "containerNo")) || Func.isEmpty(string(dispatch, "cabinNo")) || Func.isEmpty(string(dispatch, "mileage")) || decimal(dispatch, "mileage").compareTo(BigDecimal.ZERO) <= 0 || ("承运商".equals(carrierType) && Func.isEmpty(string(dispatch, "carrierName")))) { + throw new ServiceException("非公路运输的承运信息不完整"); + } + return; + } if (Func.isEmpty(string(dispatch, "vehicleNo"))) throw new ServiceException("运单车牌号不能为空"); if ("承运商".equals(carrierType)) { - if (Func.isEmpty(string(dispatch, "carrierName"))) throw new ServiceException("运单承运商不能为空"); + if (Func.isEmpty(string(dispatch, "carrierName")) || Func.isEmpty(string(dispatch, "mileage")) || decimal(dispatch, "mileage").compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException("承运商、里程不能为空且里程必须为正整数"); return; } if (Func.isEmpty(string(dispatch, "driverName")) || Func.isEmpty(string(dispatch, "driverPhone")) || Func.isEmpty(string(dispatch, "trailerVehicleNo")) || Func.isEmpty(string(dispatch, "escortName")) || Func.isEmpty(string(dispatch, "escortPhone")) || Func.isEmpty(string(dispatch, "mileage")) || decimal(dispatch, "mileage").compareTo(BigDecimal.ZERO) < 0) throw new ServiceException("自运或网货平台的车辆与人员信息不完整"); @@ -350,19 +362,57 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatchedGoods(String masterNo, String segmentNo) { - Map result = new LinkedHashMap<>(); - LambdaQueryWrapper billQuery = new LambdaQueryWrapper().eq(Waybill::getMasterNo, masterNo); - if (Func.isNotEmpty(segmentNo)) billQuery.eq(Waybill::getRelationNo, segmentNo); - for (Waybill bill : waybillService.list(billQuery)) { - List> goods = parseArray(bill.getGoodsJson()); - if (goods.isEmpty()) goods = List.of(Map.of("cargoName", bill.getCargoName(), "cargoType", bill.getCargoType(), "quantity", bill.getQuantity())); - for (Map goodsItem : goods) result.merge(goodsKey(goodsItem), decimal(goodsItem, "quantity"), BigDecimal::add); - } + List waybills = waybillsByMasterNo(masterNo).stream().filter(item -> Func.isEmpty(segmentNo) || Objects.equals(segmentNo, item.getRelationNo())).toList(); LambdaQueryWrapper planQuery = new LambdaQueryWrapper().eq(TransportPlan::getMasterNo, masterNo); if (Func.isNotEmpty(segmentNo)) planQuery.eq(TransportPlan::getRelationNo, segmentNo); - for (TransportPlan plan : transportPlanService.list(planQuery)) for (Map goods : parseArray(plan.getGoodsJson())) result.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add); + return dispatchedGoods(waybills, transportPlanService.list(planQuery)); + } + private List waybillsByMasterNo(String masterNo) { + if (Func.isEmpty(masterNo)) return List.of(); + return waybillService.list(new LambdaQueryWrapper().eq(Waybill::getMasterNo, masterNo)); + } + private Map dispatchedGoods(List waybills, List plans) { + Map result = new LinkedHashMap<>(); + for (Waybill bill : waybills) for (Map goods : waybillGoods(bill)) result.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add); + for (TransportPlan plan : plans) for (Map goods : parseArray(plan.getGoodsJson())) result.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add); return result; } + private List> waybillGoods(Waybill waybill) { + List> goods = parseArray(waybill.getGoodsJson()); + if (!goods.isEmpty()) return goods; + Map fallback = new LinkedHashMap<>(); + fallback.put("cargoName", waybill.getCargoName()); fallback.put("cargoType", waybill.getCargoType()); fallback.put("quantity", waybill.getQuantity()); + return List.of(fallback); + } + private boolean belongsToRoute(Waybill waybill, Map route) { + return belongsToRoute(waybill.getRelationNo(), waybill.getTransportType(), waybill.getDepartureName(), waybill.getDepartureAddress(), waybill.getArrivalName(), waybill.getArrivalAddress(), route); + } + private boolean belongsToRoute(TransportPlan plan, Map route) { + return belongsToRoute(plan.getRelationNo(), plan.getTransportType(), plan.getDepartureName(), plan.getDepartureAddress(), plan.getArrivalName(), plan.getArrivalAddress(), route); + } + private boolean belongsToRoute(String relationNo, String transportType, String departureName, String departureAddress, String arrivalName, String arrivalAddress, Map route) { + if (Func.isNotEmpty(relationNo)) return Objects.equals(relationNo, string(route, "segmentNo")); + return sameTransportType(transportType, string(route, "transportType")) + && sameLocation(departureName, departureAddress, string(route, "departureName"), string(route, "departureAddress")) + && sameLocation(arrivalName, arrivalAddress, string(route, "arrivalName"), string(route, "arrivalAddress")); + } + private boolean sameLocation(String name, String address, String routeName, String routeAddress) { + return (Func.isNotEmpty(address) && Objects.equals(address, routeAddress)) || (Func.isNotEmpty(name) && Objects.equals(name, routeName)); + } + private boolean sameTransportType(String left, String right) { + if (Objects.equals(left, right)) return true; + return transportTypeName(left).equals(transportTypeName(right)); + } + private String transportTypeName(String value) { + String normalized = value == null ? "" : value.toLowerCase(); + return switch (normalized) { + case "road" -> "公路运输"; + case "railway" -> "铁路运输"; + case "river" -> "水路运输"; + case "air" -> "航空运输"; + default -> value == null ? "" : value; + }; + } private BigDecimal totalQuantity(List> goods) { return goods.stream().map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add); } private String buildFreightJson(BigDecimal quantity, BigDecimal freightTotal, Map dispatch) { Map freight = new LinkedHashMap<>(); @@ -373,7 +423,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "vehicleNo", "")); } + private String waybillGroupKey(Map dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "driverPhone", ""), string(dispatch, "vehicleNo", ""), string(dispatch, "captainName", ""), string(dispatch, "containerNo", ""), string(dispatch, "cabinNo", "")); } private String joinGoodsField(List> dispatches, String field) { return dispatches.stream().map(item -> string(item, field, "")).filter(Func::isNotEmpty).distinct().reduce((left, right) -> left + "、" + right).orElse(""); } private BigDecimal decimal(Map values, String key) { try { return new BigDecimal(string(values, key, "0")); } catch (Exception exception) { return BigDecimal.ZERO; } } private BigDecimal nullableDecimal(Map values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return new BigDecimal(value); } catch (Exception exception) { return null; } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java index 0462940..5120c70 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java @@ -154,9 +154,13 @@ public class VoucherManageServiceImpl extends BaseServiceImpl waybills = listRelatedWaybills(voucher); + log.info("[凭证处理] 进度 10%:已查询关联运单 voucherId={}, waybillCount={}", voucherId, waybills.size()); Map waybillByPlate = new HashMap<>(); for (Waybill waybill : waybills) { - waybillPlateNumbers(waybill).forEach(plateNo -> waybillByPlate.putIfAbsent(plateNo, waybill)); + for (String plateNo : waybillPlateNumbers(waybill)) { + Waybill existing = waybillByPlate.putIfAbsent(plateNo, waybill); + if (existing != null && !Objects.equals(existing.getId(), waybill.getId())) { + log.warn("[凭证处理] 车牌对应多个关联运单 voucherId={}, plateNo={}, firstWaybillNo={}, duplicateWaybillNo={}", + voucherId, plateNo, existing.getWaybillNo(), waybill.getWaybillNo()); + } + } } + log.info("[凭证处理] 进度 20%:已建立车牌匹配索引 voucherId={}, plateCount={}", voucherId, waybillByPlate.size()); voucherImageMapper.deleteByVoucherId(voucher.getId()); int imageCount = 0; + int matchedImageCount = 0; Set relatedWaybillIds = new HashSet<>(); try (InputStream source = openSourceFile(voucher.getFileUrl()); ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), StandardCharsets.UTF_8)) { @@ -214,7 +235,11 @@ public class VoucherManageServiceImpl extends BaseServiceImpl Date: Fri, 14 Aug 2026 02:43:38 +0800 Subject: [PATCH 015/114] fix bug --- .../pojo/dto/WaybillImportBatchRequest.java | 35 +++ .../pojo/entity/WaybillImportBatch.java | 39 +++ .../pojo/vo/WaybillImportBatchVO.java | 25 ++ .../controller/MasterOrderController.java | 3 +- .../controller/WaybillController.java | 98 ++++++-- .../transport/excel/LoadingManageExcel.java | 6 +- .../excel/MasterOrderWaybillExcel.java | 74 ++++++ .../mapper/WaybillImportBatchMapper.java | 14 ++ .../service/IMasterOrderService.java | 3 +- .../service/IWaybillImportBatchService.java | 20 ++ .../impl/CustomerArchiveServiceImpl.java | 9 +- .../impl/LoadingManageServiceImpl.java | 16 +- .../service/impl/MasterOrderServiceImpl.java | 31 ++- .../impl/WaybillImportBatchServiceImpl.java | 228 ++++++++++++++++++ .../service/impl/WaybillServiceImpl.java | 3 + 15 files changed, 579 insertions(+), 25 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillImportBatch.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/MasterOrderWaybillExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java new file mode 100644 index 0000000..dadc790 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java @@ -0,0 +1,35 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** 运单批量导入请求。 */ +@Data +@Schema(description = "运单批量导入请求") +public class WaybillImportBatchRequest { + private Long id; + private String batchNo; + private Long projectId; + private String projectName; + private String customerName; + private Long contractId; + private String contractName; + private String carrierType; + private Long carrierId; + private List carrierIds; + private String carrierName; + private String status; + private String importStatus; + private String importType; + private Long planId; + private String planName; + private String remark; + private List> rows; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillImportBatch.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillImportBatch.java new file mode 100644 index 0000000..073d24d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillImportBatch.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** 运单批量导入批次。 */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_waybill_import_batch") +@Schema(description = "运单批量导入批次") +public class WaybillImportBatch extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + private String batchNo; + private Long projectId; + private String projectName; + private String customerName; + private Long contractId; + private String contractName; + private String carrierType; + private String carrierIds; + private String carrierName; + private String importType; + private String importStatus; + private Long planId; + private String planName; + private Integer waybillCount; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java new file mode 100644 index 0000000..841236a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java @@ -0,0 +1,25 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.WaybillImportBatch; + +import java.io.Serial; + +/** 运单批次视图。 */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "运单批次视图") +public class WaybillImportBatchVO extends WaybillImportBatch { + @Serial + private static final long serialVersionUID = 1L; + private String importTypeName; + private String statusName; + private String createUserName; + private String updateUserName; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java index 8b958e7..9b5151c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java @@ -14,6 +14,7 @@ import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest; +import org.springblade.transport.excel.MasterOrderWaybillExcel; import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.service.IMasterOrderService; import org.springframework.web.bind.annotation.GetMapping; @@ -44,5 +45,5 @@ public class MasterOrderController extends BladeController { @PostMapping("/remove") @ApiOperationSupport(order = 6) @Operation(summary = "删除") public R status(@RequestParam Long id) { return R.status(masterOrderService.removeMasterOrder(id)); } @PostMapping("/close-dispatch") @ApiOperationSupport(order = 7) @Operation(summary = "关闭调度") public R closeDispatch(@RequestParam Long id) { return R.status(masterOrderService.closeDispatch(id)); } @PostMapping("/dispatch") @ApiOperationSupport(order = 8) @Operation(summary = "确认调度") public R dispatch(@RequestBody MasterOrderDispatchRequest data) { return R.data(masterOrderService.dispatch(data)); } - @GetMapping("/export") @ApiOperationSupport(order = 9) @Operation(summary = "按运单导出") public void export(MasterOrderVO query, HttpServletResponse response) { ExcelUtil.export(response, "总单运单明细", "运单明细", masterOrderService.exportWaybills(query), MasterOrderVO.class); } + @GetMapping("/export") @ApiOperationSupport(order = 9) @Operation(summary = "按运单导出") public void export(MasterOrderVO query, HttpServletResponse response) { ExcelUtil.export(response, "总单运单明细", "运单明细", masterOrderService.exportWaybills(query), MasterOrderWaybillExcel.class); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index 3a749fe..c9c8773 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -24,11 +24,15 @@ package org.springblade.transport.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import cn.idev.excel.write.handler.SheetWriteHandler; +import cn.idev.excel.write.metadata.holder.WriteSheetHolder; +import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; +import org.apache.poi.ss.usermodel.CellStyle; import org.springblade.common.excel.ImportFailureExcelUtil; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.excel.util.ExcelUtil; @@ -45,14 +49,17 @@ import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.TransportPlan; import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; +import org.springblade.transport.pojo.vo.WaybillImportBatchVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.ICustomerArchiveService; import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.ITransportPlanService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.service.IWaybillImportBatchService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -83,6 +90,7 @@ public class WaybillController extends BladeController { private final IContractManageService contractManageService; private final ICustomerArchiveService customerArchiveService; private final ITransportPlanService transportPlanService; + private final IWaybillImportBatchService waybillImportBatchService; @GetMapping("/detail") @ApiOperationSupport(order = 1) @@ -122,22 +130,58 @@ public class WaybillController extends BladeController { return R.data(options); } - @PostMapping("/submit") + @GetMapping("/import-batch/list") @ApiOperationSupport(order = 4) + @Operation(summary = "运单批量导入批次分页") + public R> importBatchList(WaybillImportBatchRequest request, Query query) { + return R.data(waybillImportBatchService.page(Condition.getPage(query), request)); + } + + @GetMapping("/import-batch/details") + @ApiOperationSupport(order = 5) + @Operation(summary = "运单批量导入明细分页") + public R> importBatchDetails(@RequestParam Long batchId, WaybillVO waybill, Query query) { + waybill.setImportBatchId(batchId); + return R.data(waybillService.selectWaybillPage(Condition.getPage(query), waybill)); + } + + @PostMapping("/import-batch/draft") + @ApiOperationSupport(order = 6) + @Operation(summary = "保存运单批量导入草稿") + public R saveImportBatchDraft(@RequestBody WaybillImportBatchRequest request) { + return R.data(waybillImportBatchService.saveDraft(request)); + } + + @PostMapping("/import-batch/confirm") + @ApiOperationSupport(order = 7) + @Operation(summary = "确认运单批量导入") + public R confirmImportBatch(@RequestBody WaybillImportBatchRequest request) { + return R.data(waybillImportBatchService.confirm(request)); + } + + @PostMapping("/import-batch/remove") + @ApiOperationSupport(order = 8) + @Operation(summary = "删除运单批量导入批次") + public R removeImportBatches(@RequestParam String ids) { + return R.data(waybillImportBatchService.removeBatches(ids)); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 9) @Operation(summary = "新增或修改", description = "传入waybill") public R submit(@RequestBody Waybill waybill) { return R.status(waybillService.submit(waybill)); } @PostMapping("/remove") - @ApiOperationSupport(order = 5) + @ApiOperationSupport(order = 10) @Operation(summary = "逻辑删除", description = "传入ids") public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.removeWaybill(ids)); } @GetMapping("/export-waybill-manage") - @ApiOperationSupport(order = 6) + @ApiOperationSupport(order = 11) @Operation(summary = "导出运单管理") public void exportWaybill(WaybillVO waybill, @RequestParam(required = false) String ids, HttpServletResponse response) { List list = waybillService.exportWaybill(waybill, ids); @@ -145,7 +189,7 @@ public class WaybillController extends BladeController { } @PostMapping("/import-waybill-manage") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 12) @Operation(summary = "导入运单管理", description = "传入excel") public R importWaybill(MultipartFile file, HttpServletResponse response) { List failureList = waybillService.importWaybill(ExcelUtil.read(file, WaybillExcel.class)); @@ -157,63 +201,70 @@ public class WaybillController extends BladeController { } @GetMapping("/export-template") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 13) @Operation(summary = "导出模板") public void exportTemplate(HttpServletResponse response) { ExcelUtil.export(response, "运单管理模板", "运单管理导入模板", new ArrayList(), WaybillExcel.class); } @GetMapping("/import-batch/export-template") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 14) @Operation(summary = "导出运单批量导入模板") public void exportImportBatchTemplate(HttpServletResponse response) { - ExcelUtil.export(response, "运单批量导入模板", "运单批量导入模板", new ArrayList(), WaybillImportBatchExcel.class); + ExcelUtil.export( + response, + "运单批量导入模板", + "运单批量导入模板", + new ArrayList(), + new TextColumnStyleHandler(13, 14), + WaybillImportBatchExcel.class + ); } @PostMapping("/copy") - @ApiOperationSupport(order = 10) + @ApiOperationSupport(order = 15) @Operation(summary = "复制", description = "传入id") public R copy(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.data(waybillService.copy(id)); } @PostMapping("/change-route") - @ApiOperationSupport(order = 11) + @ApiOperationSupport(order = 16) @Operation(summary = "变更运输路线", description = "传入运单路线与变更记录") public R changeRoute(@RequestBody Waybill waybill) { return R.status(waybillService.changeRoute(waybill)); } @PostMapping("/cancel") - @ApiOperationSupport(order = 12) + @ApiOperationSupport(order = 17) @Operation(summary = "取消", description = "传入id") public R cancel(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.cancel(id)); } @PostMapping("/reassign") - @ApiOperationSupport(order = 13) + @ApiOperationSupport(order = 18) @Operation(summary = "重新派单", description = "传入id") public R reassign(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.reassign(id)); } @PostMapping("/complete") - @ApiOperationSupport(order = 14) + @ApiOperationSupport(order = 19) @Operation(summary = "完成", description = "传入id") public R complete(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.complete(id)); } @PostMapping("/batch-complete") - @ApiOperationSupport(order = 15) + @ApiOperationSupport(order = 20) @Operation(summary = "批量完成", description = "传入ids") public R batchComplete(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.batchComplete(ids)); } @PostMapping("/road-loading") - @ApiOperationSupport(order = 16) + @ApiOperationSupport(order = 21) @Operation(summary = "公路配载", description = "传入ids") public R roadLoading(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.roadLoading(ids)); @@ -229,4 +280,23 @@ public class WaybillController extends BladeController { return option; } + private static final class TextColumnStyleHandler implements SheetWriteHandler { + + private final int[] columnIndexes; + + private TextColumnStyleHandler(int... columnIndexes) { + this.columnIndexes = columnIndexes; + } + + @Override + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + CellStyle textStyle = writeWorkbookHolder.getWorkbook().createCellStyle(); + short textFormat = writeWorkbookHolder.getWorkbook().createDataFormat().getFormat("@"); + textStyle.setDataFormat(textFormat); + for (int columnIndex : columnIndexes) { + writeSheetHolder.getSheet().setDefaultColumnStyle(columnIndex, textStyle); + } + } + } + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/LoadingManageExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/LoadingManageExcel.java index 5687407..950f89f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/LoadingManageExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/LoadingManageExcel.java @@ -41,15 +41,15 @@ public class LoadingManageExcel implements Serializable { private String driverPhone; @ExcelProperty("承运类型") private String carrierType; - @ExcelProperty("承运商") - private String carrierName; @ExcelProperty("发货地") private String departureAddress; @ExcelProperty("途经地") private String transitAddress; @ExcelProperty("到货地") private String arrivalAddress; - @ExcelProperty("运输类型") + @ExcelProperty("承运商") + private String carrierName; + @ExcelProperty("运输方式") private String transportType; @ExcelProperty("数据来源") private String dataSource; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/MasterOrderWaybillExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/MasterOrderWaybillExcel.java new file mode 100644 index 0000000..5f8f1ae --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/MasterOrderWaybillExcel.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import cn.idev.excel.annotation.write.style.ContentRowHeight; +import cn.idev.excel.annotation.write.style.HeadRowHeight; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Date; + +/** 总单运单明细导出模型。 */ +@Data +@ColumnWidth(20) +@HeadRowHeight(20) +@ContentRowHeight(18) +public class MasterOrderWaybillExcel { + + @ExcelProperty("总单号") + private String masterNo; + @ExcelProperty("运单号") + private String waybillNo; + @ExcelProperty("项目名称") + private String projectName; + @ExcelProperty("客户合同") + private String contractName; + @ExcelProperty("客户名称") + private String customerName; + @ExcelProperty("运输类型") + private String transportType; + @ExcelProperty("货物名称") + private String cargoName; + @ExcelProperty("货物类型") + private String cargoType; + @ExcelProperty("数量") + private BigDecimal quantity; + @ExcelProperty("数量单位") + private String quantityUnit; + @ExcelProperty("发货地址") + private String departureAddress; + @ExcelProperty("发货联系人") + private String departureContact; + @ExcelProperty("发货联系人电话") + private String departurePhone; + @ExcelProperty("收货地址") + private String arrivalAddress; + @ExcelProperty("收货联系人") + private String arrivalContact; + @ExcelProperty("收货联系人电话") + private String arrivalPhone; + @ExcelProperty("承运类型") + private String carrierType; + @ExcelProperty("承运商") + private String carrierName; + @ExcelProperty("司机") + private String driverName; + @ExcelProperty("车牌号/航班号/船号/班列号") + private String vehicleNo; + @ExcelProperty("开始日期") + private LocalDate startDate; + @ExcelProperty("结束日期") + private LocalDate endDate; + @ExcelProperty("业务状态") + private String businessStatus; + @ExcelProperty("备注") + private String remark; + @ExcelProperty("创建时间") + private Date createTime; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java new file mode 100644 index 0000000..5631be8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java @@ -0,0 +1,14 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.WaybillImportBatch; + +/** 运单批次 Mapper。 */ +@Mapper +public interface WaybillImportBatchMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java index db8963e..bb78280 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java @@ -6,6 +6,7 @@ import org.springblade.core.mp.base.BaseService; import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest; import org.springblade.transport.pojo.entity.MasterOrder; import org.springblade.transport.pojo.vo.MasterOrderVO; +import org.springblade.transport.excel.MasterOrderWaybillExcel; import java.util.List; @@ -22,5 +23,5 @@ public interface IMasterOrderService extends BaseService { boolean removeMasterOrder(Long id); boolean closeDispatch(Long id); MasterOrderVO dispatch(MasterOrderDispatchRequest request); - List exportWaybills(MasterOrderVO query); + List exportWaybills(MasterOrderVO query); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java new file mode 100644 index 0000000..799f63e --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java @@ -0,0 +1,20 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; +import org.springblade.transport.pojo.entity.WaybillImportBatch; +import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.WaybillImportBatchVO; + +/** 运单批次服务。 */ +public interface IWaybillImportBatchService extends BaseService { + WaybillImportBatch saveDraft(WaybillImportBatchRequest request); + WaybillImportBatch confirm(WaybillImportBatchRequest request); + IPage page(IPage page, WaybillImportBatchRequest request); + BusinessRemoveResultVO removeBatches(String ids); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index f9b3e17..aa8f058 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -749,12 +749,19 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl userIds = new ArrayList<>(baseMapper.selectIncludeNewCustomerUserIds(customer.getTenantId())); + String tenantId = Func.isNotEmpty(customer.getTenantId()) ? customer.getTenantId() : AuthUtil.getTenantId(); + List userIds = new ArrayList<>(baseMapper.selectIncludeNewCustomerUserIds(tenantId)); Long currentUserId = AuthUtil.getUserId(); if (Func.isNotEmpty(currentUserId) && !userIds.contains(currentUserId)) { userIds.add(currentUserId); } userIds.forEach(userId -> { + Long scopeCount = userCustomerScopeMapper.selectCount(Wrappers.lambdaQuery() + .eq(UserCustomerScope::getUserId, userId) + .eq(UserCustomerScope::getCustomerId, customer.getId())); + if (scopeCount != null && scopeCount > 0) { + return; + } UserCustomerScope scope = new UserCustomerScope(); scope.setUserId(userId); scope.setCustomerId(customer.getId()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 147bad2..199bcfe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -261,7 +261,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); + if (Func.isEmpty(waybillIdList)) { + return false; + } + List waybillList = waybillMapper.selectList(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .in(Waybill::getId, waybillIdList)); + return waybillList.size() == waybillIdList.size() + && waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_RUNNING)); + } + private LambdaQueryWrapper buildQuery(LoadingManageVO loadingManage) { TransportBusinessSupport.validateAllDept(loadingManage.getAllDept(), "配载管理"); LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery().eq(LoadingManage::getIsDeleted, 0); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index 52663ce..d7e5a88 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -13,6 +13,7 @@ import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserCache; import org.springblade.transport.mapper.MasterOrderMapper; +import org.springblade.transport.excel.MasterOrderWaybillExcel; import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest; import org.springblade.transport.pojo.entity.MasterOrder; import org.springblade.transport.pojo.entity.ContractManage; @@ -152,14 +153,36 @@ public class MasterOrderServiceImpl extends BaseServiceImpl exportWaybills(MasterOrderVO query) { - List result = new ArrayList<>(); + public List exportWaybills(MasterOrderVO query) { + List result = new ArrayList<>(); for (MasterOrder masterOrder : list(buildQuery(query))) { for (Waybill waybill : waybillService.list(new LambdaQueryWrapper().eq(Waybill::getMasterNo, masterOrder.getMasterNo()))) { - MasterOrderVO row = toVO(masterOrder); + MasterOrderWaybillExcel row = new MasterOrderWaybillExcel(); + row.setMasterNo(masterOrder.getMasterNo()); + row.setWaybillNo(waybill.getWaybillNo()); + row.setProjectName(waybill.getProjectName()); + row.setContractName(waybill.getContractName()); + row.setCustomerName(waybill.getCustomerName()); + row.setTransportType(waybill.getTransportType()); row.setCargoName(waybill.getCargoName()); row.setCargoType(waybill.getCargoType()); - row.setMasterNo(waybill.getWaybillNo()); + row.setQuantity(waybill.getQuantity()); + row.setQuantityUnit(waybill.getQuantityUnit()); + row.setDepartureAddress(waybill.getDepartureAddress()); + row.setDepartureContact(waybill.getDepartureContact()); + row.setDeparturePhone(waybill.getDeparturePhone()); + row.setArrivalAddress(waybill.getArrivalAddress()); + row.setArrivalContact(waybill.getArrivalContact()); + row.setArrivalPhone(waybill.getArrivalPhone()); + row.setCarrierType(waybill.getCarrierType()); + row.setCarrierName(waybill.getCarrierName()); + row.setDriverName(waybill.getDriverName()); + row.setVehicleNo(waybill.getVehicleNo()); + row.setStartDate(waybill.getStartDate()); + row.setEndDate(waybill.getEndDate()); + row.setBusinessStatus(waybill.getBusinessStatus()); + row.setRemark(waybill.getRemark()); + row.setCreateTime(waybill.getCreateTime()); result.add(row); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java new file mode 100644 index 0000000..1a2f5ff --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java @@ -0,0 +1,228 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.mapper.WaybillImportBatchMapper; +import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; +import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.ProjectApply; +import org.springblade.transport.pojo.entity.TransportPlan; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.entity.WaybillImportBatch; +import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.WaybillImportBatchVO; +import org.springblade.transport.service.ICustomerArchiveService; +import org.springblade.transport.service.IProjectApplyService; +import org.springblade.transport.service.ITransportPlanService; +import org.springblade.transport.service.IWaybillImportBatchService; +import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.TransportBusinessSupport; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** 运单批次服务实现。 */ +@Service +@RequiredArgsConstructor +public class WaybillImportBatchServiceImpl extends BaseServiceImpl implements IWaybillImportBatchService { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final IWaybillService waybillService; + private final ICustomerArchiveService customerArchiveService; + private final IProjectApplyService projectApplyService; + private final ITransportPlanService transportPlanService; + + @Override + @Transactional(rollbackFor = Exception.class) + public WaybillImportBatch saveDraft(WaybillImportBatchRequest request) { + WaybillImportBatch batch = buildBatch(request, "draft"); + batch.setWaybillCount(0); + if (Func.isNotEmpty(request.getId())) { + WaybillImportBatch oldBatch = getById(request.getId()); + if (oldBatch == null || Objects.equals(oldBatch.getIsDeleted(), 1)) throw new ServiceException("运单批次不存在"); + batch.setId(oldBatch.getId()); + batch.setBatchNo(oldBatch.getBatchNo()); + } + saveOrUpdate(batch); + return batch; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public WaybillImportBatch confirm(WaybillImportBatchRequest request) { + if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细"); + String importStatus = "processing".equals(request.getStatus()) ? "processing" : "completed"; + WaybillImportBatch batch = buildBatch(request, importStatus); + if (Func.isNotEmpty(request.getId())) { + WaybillImportBatch oldBatch = getById(request.getId()); + if (oldBatch == null || Objects.equals(oldBatch.getIsDeleted(), 1)) throw new ServiceException("运单批次不存在"); + if (!"draft".equals(oldBatch.getImportStatus())) throw new ServiceException("仅草稿状态的批次允许确认导入"); + batch.setId(oldBatch.getId()); + batch.setBatchNo(oldBatch.getBatchNo()); + } + batch.setWaybillCount(0); + saveOrUpdate(batch); + + for (int index = 0; index < request.getRows().size(); index++) { + try { + Waybill waybill = buildWaybill(request.getRows().get(index), batch); + waybillService.submit(waybill); + } catch (Exception exception) { + throw new ServiceException("第" + (index + 1) + "行导入失败:" + exception.getMessage()); + } + } + batch.setWaybillCount(request.getRows().size()); + batch.setImportStatus(importStatus); + updateById(batch); + return batch; + } + + @Override + public IPage page(IPage page, WaybillImportBatchRequest request) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(WaybillImportBatch::getIsDeleted, 0) + .like(Func.isNotEmpty(request.getBatchNo()), WaybillImportBatch::getBatchNo, request.getBatchNo()) + .apply(Func.isNotEmpty(request.getCarrierId()), "FIND_IN_SET({0}, carrier_ids)", request.getCarrierId()) + .eq(Func.isNotEmpty(request.getCarrierName()), WaybillImportBatch::getCarrierName, request.getCarrierName()) + .orderByDesc(WaybillImportBatch::getCreateTime); + IPage entityPage = page(page, queryWrapper); + List records = entityPage.getRecords().stream().map(this::toVO).toList(); + Page resultPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal()); + resultPage.setRecords(records); + return resultPage; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public BusinessRemoveResultVO removeBatches(String ids) { + List idList = Func.toLongList(ids); + if (Func.isEmpty(idList)) throw new ServiceException("请选择需要删除的运单批次"); + BusinessRemoveResultVO result = new BusinessRemoveResultVO(); + for (WaybillImportBatch batch : listByIds(idList)) { + if (Objects.equals(batch.getIsDeleted(), 1)) continue; + List waybills = waybillService.list(Wrappers.lambdaQuery() + .eq(Waybill::getImportBatchId, batch.getId()).eq(Waybill::getIsDeleted, 0)); + if (Func.isNotEmpty(waybills)) { + waybillService.deleteLogic(waybills.stream().map(Waybill::getId).toList()); + } + deleteLogic(List.of(batch.getId())); + result.setSuccessCount(result.getSuccessCount() + 1); + } + return result; + } + + private WaybillImportBatch buildBatch(WaybillImportBatchRequest request, String importStatus) { + if (Func.isEmpty(request.getProjectId())) throw new ServiceException("请选择项目"); + if (Func.isEmpty(request.getContractId())) throw new ServiceException("请选择客户合同"); + if (Func.isEmpty(request.getCarrierType())) throw new ServiceException("请选择承运类型"); + if (Func.isEmpty(request.getImportType())) throw new ServiceException("请选择导入类型"); + WaybillImportBatch batch = new WaybillImportBatch(); + BeanUtil.copyProperties(request, batch); + batch.setBatchNo(Func.isEmpty(request.getBatchNo()) ? nextCode() : request.getBatchNo()); + batch.setCarrierIds(joinIds(request.getCarrierIds())); + batch.setCarrierName(resolveCarrierNames(request.getCarrierIds(), request.getCarrierName())); + batch.setImportStatus(importStatus); + batch.setImportType("settlement".equals(request.getImportType()) ? "settlement" : "waybill"); + ProjectApply project = projectApplyService.getById(request.getProjectId()); + if (project == null || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) { + throw new ServiceException("仅允许选择审核通过或变更审核通过的项目"); + } + batch.setProjectName(project.getProjectName()); + if (Func.isEmpty(batch.getCustomerName())) batch.setCustomerName(project.getCustomerNames()); + TransportPlan plan = Func.isEmpty(request.getPlanId()) ? null : transportPlanService.getById(request.getPlanId()); + if (plan != null) batch.setPlanName(plan.getPlanName()); + return batch; + } + + private Waybill buildWaybill(Map row, WaybillImportBatch batch) { + Waybill waybill = BeanUtil.copyProperties(row, Waybill.class); + if (waybill == null) throw new ServiceException("运单数据不正确"); + waybill.setProjectId(batch.getProjectId()); + waybill.setProjectName(batch.getProjectName()); + waybill.setCustomerName(batch.getCustomerName()); + waybill.setContractId(batch.getContractId()); + waybill.setContractName(batch.getContractName()); + waybill.setCarrierType(batch.getCarrierType()); + waybill.setCarrierId(firstCarrierId(batch.getCarrierIds())); + waybill.setCarrierName(batch.getCarrierName()); + waybill.setPlanId(batch.getPlanId()); + waybill.setPlanName(batch.getPlanName()); + waybill.setImportBatchId(batch.getId()); + waybill.setBatchNo(batch.getBatchNo()); + waybill.setDataSource("批量导入"); + waybill.setBusinessStatus("processing".equals(batch.getImportStatus()) ? "pending" : batch.getImportStatus()); + waybill.setQuantity(defaultQuantity(waybill.getQuantity())); + waybill.setQuantityUnit(Func.isEmpty(waybill.getQuantityUnit()) ? "吨" : waybill.getQuantityUnit()); + waybill.setPriceUnit(Func.isEmpty(waybill.getPriceUnit()) ? "吨" : waybill.getPriceUnit()); + waybill.setStartDate(parseDate(row.get("startDate"), "开始时间")); + waybill.setEndDate(parseDate(row.get("endDate"), "结束时间")); + return waybill; + } + + private LocalDate parseDate(Object value, String fieldName) { + if (value == null || String.valueOf(value).isBlank()) throw new ServiceException(fieldName + "不能为空"); + String text = String.valueOf(value).trim(); + try { + return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER); + } catch (DateTimeParseException exception) { + throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD HH:mm:ss"); + } + } + + private BigDecimal defaultQuantity(BigDecimal quantity) { + return quantity == null ? BigDecimal.ONE : quantity; + } + + private Long firstCarrierId(String carrierIds) { + if (Func.isEmpty(carrierIds)) return null; + return Func.toLongList(carrierIds).stream().findFirst().orElse(null); + } + + private String resolveCarrierNames(List carrierIds, String carrierName) { + if (Func.isNotEmpty(carrierName)) return carrierName; + if (Func.isEmpty(carrierIds)) return null; + List carriers = customerArchiveService.listByIds(carrierIds); + return carriers.stream().map(CustomerArchive::getFullName).filter(Func::isNotEmpty).collect(Collectors.joining(",")); + } + + private String joinIds(List carrierIds) { + return Func.isEmpty(carrierIds) ? null : carrierIds.stream().map(String::valueOf).collect(Collectors.joining(",")); + } + + private WaybillImportBatchVO toVO(WaybillImportBatch batch) { + WaybillImportBatchVO vo = BeanUtil.copyProperties(batch, WaybillImportBatchVO.class); + if (vo == null) throw new ServiceException("运单批次数据转换失败"); + vo.setImportTypeName("settlement".equals(batch.getImportType()) ? "结算单" : "运单"); + vo.setStatusName(switch (batch.getImportStatus()) { case "draft" -> "草稿"; case "processing" -> "进行中"; default -> "完成"; }); + vo.setCreateUserName(UserCache.getUserRealName(batch.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(batch.getUpdateUser())); + return vo; + } + + private synchronized String nextCode() { + String prefix = "YDB" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + long count = count(Wrappers.lambdaQuery().likeRight(WaybillImportBatch::getBatchNo, prefix)); + return prefix + String.format("%04d", count + 1); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 8d831c2..fad4d26 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -427,6 +427,9 @@ public class WaybillServiceImpl extends BaseServiceImpl if (Func.isNotEmpty(waybill.getBatchNo())) { queryWrapper.like(Waybill::getBatchNo, waybill.getBatchNo()); } + if (Func.isNotEmpty(waybill.getImportBatchId())) { + queryWrapper.eq(Waybill::getImportBatchId, waybill.getImportBatchId()); + } if (Func.isNotEmpty(waybill.getRelationNo())) { queryWrapper.like(Waybill::getRelationNo, waybill.getRelationNo()); } From 1ca36ae1530792243e2fcfb36f3127f3293e8b54 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 14 Aug 2026 14:45:39 +0800 Subject: [PATCH 016/114] fix bug --- .../springblade/transport/pojo/vo/TransportPlanVO.java | 3 +-- .../file/service/impl/FileTaskServiceImpl.java | 6 +++++- .../transport/service/impl/LoadingManageServiceImpl.java | 2 +- .../transport/service/impl/MasterOrderServiceImpl.java | 2 +- .../transport/service/impl/TransportPlanServiceImpl.java | 9 ++++++--- .../transport/service/impl/WaybillServiceImpl.java | 2 +- .../blade_file_task_upload_time_upgrade_20260814.sql | 6 ++++++ 7 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 doc/sql/bladex/blade_file_task_upload_time_upgrade_20260814.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java index f053294..ee70c42 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java @@ -27,7 +27,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.TransportPlan; -import org.springblade.transport.pojo.entity.Waybill; import java.io.Serial; import java.util.List; @@ -66,7 +65,7 @@ public class TransportPlanVO extends TransportPlan { @TableField(exist = false) @Schema(description = "计划调度生成的运单") - private List dispatchRows; + private List dispatchRows; } diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java index 1f64964..b92fc66 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java @@ -57,6 +57,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -127,6 +128,8 @@ public class FileTaskServiceImpl extends ServiceImpl i addParam.setUploadId(uploadId); // 正在上传 addParam.setStatus(FileTaskStatus.UPLOADING.getCode()); + // 文件任务创建即代表上传开始,显式记录时间,避免依赖自动填充导致上传时间为空。 + addParam.setCreateTime(new Date()); // 兼容历史表未配置 is_deleted 默认值的场景。 addParam.setIsDeleted(0); this.save(addParam); @@ -344,7 +347,8 @@ public class FileTaskServiceImpl extends ServiceImpl i .eq(StringUtil.isNotBlank(businessId), FileTask::getBusinessId, businessId) .like(StringUtil.isNotBlank(attachmentName), FileTask::getAttachmentName, attachmentName) .eq(StringUtil.isNotBlank(status), FileTask::getStatus, status) - .orderByDesc(FileTask::getCreateTime)); + .orderByDesc(FileTask::getCreateTime) + .orderByDesc(FileTask::getId)); return page.convert(this::getFileTaskUpdateVO); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 199bcfe..09396f5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -615,7 +615,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl latestList = list(Wrappers.lambdaQuery() .select(LoadingManage::getLoadingNo) .likeRight(LoadingManage::getLoadingNo, prefix) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index d7e5a88..3a15e5e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -455,5 +455,5 @@ public class MasterOrderServiceImpl extends BaseServiceImpl values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return LocalDate.parse(value.substring(0, 10)); } catch (Exception exception) { throw new ServiceException("日期格式不正确"); } } private String string(Map values, String key) { return string(values, key, null); } private String string(Map values, String key, String fallback) { Object value = values.get(key); return value == null ? fallback : String.valueOf(value); } - private synchronized String nextCode() { String prefix = "DL" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = count(new LambdaQueryWrapper().likeRight(MasterOrder::getMasterNo, prefix)); return prefix + String.format("%04d", count + 1); } + private synchronized String nextCode() { String prefix = "DL-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"; long count = count(new LambdaQueryWrapper().likeRight(MasterOrder::getMasterNo, prefix)); return prefix + String.format("%04d", count + 1); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java index a197175..2843a48 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java @@ -46,10 +46,12 @@ import org.springblade.transport.pojo.entity.TransportPlan; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.TransportPlanVO; +import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.service.ITransportPlanService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.wrapper.TransportPlanWrapper; +import org.springblade.transport.wrapper.WaybillWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -369,11 +371,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpl plan.setDispatchRows(Collections.emptyList())); return; } - Map> waybillsByPlanId = waybillService.list(Wrappers.lambdaQuery() + Map> waybillsByPlanId = waybillService.list(Wrappers.lambdaQuery() .eq(Waybill::getIsDeleted, 0) .in(Waybill::getPlanId, planIds)) .stream() - .collect(Collectors.groupingBy(Waybill::getPlanId)); + .map(WaybillWrapper.build()::entityVO) + .collect(Collectors.groupingBy(WaybillVO::getPlanId)); plans.forEach(plan -> plan.setDispatchRows( waybillsByPlanId.getOrDefault(plan.getId(), Collections.emptyList()) )); @@ -522,7 +525,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl latestList = list(Wrappers.lambdaQuery() .select(TransportPlan::getPlanNo) .likeRight(TransportPlan::getPlanNo, prefix) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index fad4d26..66fb490 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -816,7 +816,7 @@ public class WaybillServiceImpl extends BaseServiceImpl } private synchronized String nextCode() { - String prefix = "YD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + String prefix = "YD-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"; List latestList = list(Wrappers.lambdaQuery() .select(Waybill::getWaybillNo) .likeRight(Waybill::getWaybillNo, prefix) diff --git a/doc/sql/bladex/blade_file_task_upload_time_upgrade_20260814.sql b/doc/sql/bladex/blade_file_task_upload_time_upgrade_20260814.sql new file mode 100644 index 0000000..8bb1703 --- /dev/null +++ b/doc/sql/bladex/blade_file_task_upload_time_upgrade_20260814.sql @@ -0,0 +1,6 @@ +-- 补齐历史文件上传任务的上传时间。 +-- 文件任务创建时间即上传开始时间;仅回填空值,不覆盖已有记录。 +UPDATE `blade_file_task` +SET `create_time` = `update_time` +WHERE `create_time` IS NULL + AND `update_time` IS NOT NULL; From 6c74983707394ed69afd311821cf7144305d1637 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 15 Aug 2026 20:52:17 +0800 Subject: [PATCH 017/114] =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=96=B0=E7=89=88MK?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/src/main/resources/application.yml | 4 ++-- blade-service-api/pom.xml | 1 + .../blade-file/src/main/resources/application.yml | 4 ++-- blade-service/blade-openapi/pom.xml | 4 ++++ .../blade-openapi/src/main/resources/application.yml | 4 ++-- .../blade-system/src/main/resources/application.yml | 4 ++-- pom.xml | 7 ++++++- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index c677b13..feef54f 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -16,11 +16,11 @@ spring: password: ${NACOS_PASSWORD:nacos} server-addr: ${NACOS_HOST:127.0.0.1:8848} discovery: - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" config: # 文件后缀名 file-extension: yaml - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" datasource: url: ${blade.datasource.${spring.profiles.active}.url} username: ${blade.datasource.${spring.profiles.active}.username} diff --git a/blade-service-api/pom.xml b/blade-service-api/pom.xml index f62c479..5459124 100644 --- a/blade-service-api/pom.xml +++ b/blade-service-api/pom.xml @@ -21,6 +21,7 @@ blade-ratelimit-api blade-scope-api blade-system-api + blade-process-api blade-user-api blade-record-api blade-file-api diff --git a/blade-service/blade-file/src/main/resources/application.yml b/blade-service/blade-file/src/main/resources/application.yml index c63e919..20bd504 100644 --- a/blade-service/blade-file/src/main/resources/application.yml +++ b/blade-service/blade-file/src/main/resources/application.yml @@ -16,10 +16,10 @@ spring: password: ${NACOS_PASSWORD:nacos} server-addr: ${NACOS_HOST:127.0.0.1:8848} discovery: - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" config: file-extension: yaml - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" datasource: url: ${blade.datasource.${spring.profiles.active}.url} username: ${blade.datasource.${spring.profiles.active}.username} diff --git a/blade-service/blade-openapi/pom.xml b/blade-service/blade-openapi/pom.xml index 64e1961..50180e1 100644 --- a/blade-service/blade-openapi/pom.xml +++ b/blade-service/blade-openapi/pom.xml @@ -26,6 +26,10 @@ org.springblade blade-starter-swagger + + org.springblade + blade-starter-threadpool + org.springblade blade-open-api diff --git a/blade-service/blade-openapi/src/main/resources/application.yml b/blade-service/blade-openapi/src/main/resources/application.yml index 1905b44..90dea1e 100644 --- a/blade-service/blade-openapi/src/main/resources/application.yml +++ b/blade-service/blade-openapi/src/main/resources/application.yml @@ -17,10 +17,10 @@ spring: password: ${NACOS_PASSWORD:nacos} server-addr: ${NACOS_HOST:127.0.0.1:8848} discovery: - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" config: file-extension: yaml - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" datasource: url: ${blade.datasource.${spring.profiles.active}.url} username: ${blade.datasource.${spring.profiles.active}.username} diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 294659d..02ca580 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -16,10 +16,10 @@ spring: password: ${NACOS_PASSWORD:nacos} server-addr: ${NACOS_HOST:127.0.0.1:8848} discovery: - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" config: file-extension: yaml - namespace: ${NACOS_NAMESPACE:${spring.profiles.active}} + namespace: "${NACOS_NAMESPACE:}" datasource: url: ${blade.datasource.${spring.profiles.active}.url} username: ${blade.datasource.${spring.profiles.active}.username} diff --git a/pom.xml b/pom.xml index 9ab2110..bb5f2bf 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ - 4.10.0.RELEASE + 4.10.0.BASE-SNAPSHOT 17 3.14.1 @@ -109,6 +109,11 @@ blade-system-api ${revision} + + org.springblade + blade-process-api + ${revision} + org.springblade blade-transport-api From 176035ab9534078fd135996953419a7f0a687e36 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 01:41:37 +0800 Subject: [PATCH 018/114] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E7=99=BE?= =?UTF-8?q?=E5=BA=A6OCR=202=E3=80=81=E6=96=B0=E5=A2=9E=E5=BA=94=E6=94=B6?= =?UTF-8?q?=E5=BA=94=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReceivablePayableAdjustFeeRequest.java | 59 ++ .../dto/ReceivablePayableGenerateRequest.java | 3 + .../ReceivablePayableUpdateFeeRequest.java | 10 + .../transport/pojo/entity/ContractManage.java | 12 + .../pojo/entity/InsuranceOcrTemplate.java | 60 ++ .../transport/pojo/vo/BaiduOcrResultVO.java | 59 ++ .../pojo/vo/InsuranceOcrTemplateVO.java | 59 ++ .../file/controller/FileController.java | 41 +- .../file/service/IAttachmentService.java | 3 +- .../service/impl/AttachmentServiceImpl.java | 7 +- .../controller/BaiduOcrController.java | 119 ++++ .../controller/ContractManageController.java | 6 + .../InsuranceOcrTemplateController.java | 98 ++++ .../controller/InsuranceRecordController.java | 2 +- .../ReceivablePayableDetailController.java | 20 +- .../mapper/InsuranceOcrTemplateMapper.java | 38 ++ .../ocr/config/BaiduOcrConfiguration.java | 55 ++ .../ocr/config/BaiduOcrProperties.java | 79 +++ .../transport/ocr/constant/BaiduOcrType.java | 100 ++++ .../ocr/service/IBaiduOcrService.java | 57 ++ .../ocr/service/impl/BaiduOcrServiceImpl.java | 356 ++++++++++++ .../service/IContractManageService.java | 1 + .../service/IInsuranceOcrTemplateService.java | 57 ++ .../IReceivablePayableDetailService.java | 9 + .../impl/ContractManageServiceImpl.java | 44 ++ .../impl/InsuranceOcrTemplateServiceImpl.java | 154 +++++ .../impl/InsuranceRecordServiceImpl.java | 152 ++++- .../ReceivablePayableDetailServiceImpl.java | 528 ++++++++++++++++-- .../service/impl/WaybillServiceImpl.java | 15 +- .../wrapper/InsuranceOcrTemplateWrapper.java | 55 ++ doc/nacos/blade-dev.yaml | 11 + doc/nacos/blade-prod.yaml | 11 + doc/nacos/blade-test.yaml | 11 + ...de_contract_manage_fee_config_20260817.sql | 6 + .../blade_insurance_ocr_template.sql | 32 ++ 35 files changed, 2255 insertions(+), 74 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BaiduOcrResultVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceOcrTemplateVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BaiduOcrController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceOcrTemplateController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceOcrTemplateMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrConfiguration.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrProperties.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/constant/BaiduOcrType.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/IBaiduOcrService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/impl/BaiduOcrServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInsuranceOcrTemplateService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceOcrTemplateServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InsuranceOcrTemplateWrapper.java create mode 100644 doc/sql/transport/blade_contract_manage_fee_config_20260817.sql create mode 100644 doc/sql/transport/blade_insurance_ocr_template.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java new file mode 100644 index 0000000..e434b2a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 应收应付费用调整请求 + * + * @author Chill + */ +@Data +@Schema(description = "应收应付费用调整请求") +public class ReceivablePayableAdjustFeeRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "应收应付明细ID") + private Long detailId; + + @Schema(description = "调整原因") + private String adjustReason; + + @Schema(description = "费用调整行") + private List rows; + + @Data + @Schema(description = "费用调整行") + public static class AdjustRow implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "费用行ID") + private Long id; + + @Schema(description = "运输量") + private BigDecimal transportQuantity; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "运输费") + private BigDecimal freightAmount; + + @Schema(description = "动态费用项目") + private Map feeItems; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableGenerateRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableGenerateRequest.java index 02dffaf..882bb36 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableGenerateRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableGenerateRequest.java @@ -46,6 +46,9 @@ public class ReceivablePayableGenerateRequest implements Serializable { @Schema(description = "合同ID") private Long contractId; + @Schema(description = "结算类型:receivable/payable") + private String settlementType; + @Schema(description = "计费方案ID") private String billingPlanId; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableUpdateFeeRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableUpdateFeeRequest.java index 0c1d3ea..9268eb2 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableUpdateFeeRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableUpdateFeeRequest.java @@ -28,6 +28,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; import java.util.List; +import java.math.BigDecimal; /** * 应收应付更新费用请求 @@ -47,12 +48,21 @@ public class ReceivablePayableUpdateFeeRequest implements Serializable { @Schema(description = "合同ID") private Long contractId; + @Schema(description = "结算类型:receivable/payable") + private String settlementType; + @Schema(description = "计费方案ID") private String billingPlanId; @Schema(description = "调整原因") private String adjustReason; + @Schema(description = "手工调差金额,可正可负") + private BigDecimal adjustAmount; + + @Schema(description = "手工调差费用项") + private String adjustFeeItem; + @Schema(description = "仅关闭") private Boolean closeOnly; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java index 5ee249e..55325a5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java @@ -134,6 +134,9 @@ public class ContractManage extends TenantEntity { @Schema(description = "计费信息开关") private Integer billingEnabled; + @Schema(description = "费用生成模式:system系统生成,manual手动生成") + private String feeGenerationMode; + @Schema(description = "合同主文件JSON") private String contractFileJson; @@ -146,6 +149,15 @@ public class ContractManage extends TenantEntity { @Schema(description = "结算生成规则JSON") private String settlementRuleJson; + @Schema(description = "预结算配置JSON") + private String preSettlementConfigJson; + + @Schema(description = "正式结算配置JSON") + private String formalSettlementConfigJson; + + @Schema(description = "付款比例设置JSON") + private String paymentRatioJson; + @Schema(description = "对账配置JSON") private String reconciliationJson; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java new file mode 100644 index 0000000..6fe108d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java @@ -0,0 +1,60 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** + * 保险OCR识别模板实体类。 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_insurance_ocr_template") +@Schema(description = "保险OCR识别模板") +public class InsuranceOcrTemplate extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** 模板名称。 */ + @Schema(description = "模板名称") + private String name; + + /** 字段映射配置JSON。 */ + @TableField("mapping_config") + @Schema(description = "字段映射配置JSON") + private String mappingConfig; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BaiduOcrResultVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BaiduOcrResultVO.java new file mode 100644 index 0000000..d22af13 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BaiduOcrResultVO.java @@ -0,0 +1,59 @@ +/** + * 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.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 百度 OCR 识别结果。 + * + * @author Chill + */ +@Data +@Schema(description = "百度OCR识别结果") +public class BaiduOcrResultVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 业务证件类型。 */ + @Schema(description = "证件类型") + private String type; + + /** 正副面,非卡证类型为空。 */ + @Schema(description = "证件面,front为正面或主页,back为反面或副页") + private String side; + + /** 百度 OCR 原始返回结果,包含 words_result 等字段。 */ + @Schema(description = "百度OCR原始返回结果") + private Map result; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceOcrTemplateVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceOcrTemplateVO.java new file mode 100644 index 0000000..4c0c3c6 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceOcrTemplateVO.java @@ -0,0 +1,59 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.InsuranceOcrTemplate; + +import java.io.Serial; + +/** + * 保险OCR识别模板视图实体类。 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "保险OCR识别模板") +public class InsuranceOcrTemplateVO extends InsuranceOcrTemplate { + + @Serial + private static final long serialVersionUID = 1L; + + /** 创建人姓名。 */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + + /** 更新人姓名。 */ + @TableField(exist = false) + @Schema(description = "更新人姓名") + private String updateUserName; + +} diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileController.java b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileController.java index 37cab5d..5ac37ae 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileController.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileController.java @@ -11,6 +11,7 @@ import lombok.AllArgsConstructor; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.log.annotation.ApiLog; import org.springblade.core.tenant.annotation.NonDS; +import org.springblade.core.tool.api.FR; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.StringUtil; import org.springblade.file.listener.FileEvent; @@ -57,29 +58,29 @@ public class FileController extends BladeController { @ApiLog("附件管理-批量上传") @Operation(summary = "OBS批量上传", description = "OBS批量上传,参数名:files") @PostMapping("/ossUpload") - public R ossUpload(MultipartFile[] files, @RequestParam(value = "fileName", required = false) String fileName) { + public FR ossUpload(MultipartFile[] files, @RequestParam(value = "fileName", required = false) String fileName) { return attachmentService.batchOssUpload(files, fileName); } @Operation(summary = "上传", description = "上传,参数名:file") @PostMapping("/upload") - public R upload(MultipartFile file) { + public FR upload(MultipartFile file) { MultipartFile[] files = {file}; R> result = attachmentService.batchOssUpload(files, null); List data = result.getData(); - if(data != null && !data.isEmpty())return R.data(data.get(0)); - return R.fail("上传失败"); + if(data != null && !data.isEmpty())return FR.data(data.get(0)); + return FR.fail("上传失败"); } @Operation(summary = "获取附件,多个附件id用逗号分割", description = "获取附件,多个附件id用逗号分割") @GetMapping("/getAttachment") - public R getAttachment(@RequestParam(value = "id") String id) { - return R.data(attachmentService.getAttachment(id)); + public FR getAttachment(@RequestParam(value = "id") String id) { + return FR.data(attachmentService.getAttachment(id)); } @Operation(summary = "获取文件url", description = "获取文件url,参数:objectKey") @GetMapping("/getFileUrl") - public R getFileUrl(@RequestParam(value = "objectKey") String objectKey, @RequestParam(value = "attachmentName", required = false) String attachmentName) { + public FR getFileUrl(@RequestParam(value = "objectKey") String objectKey, @RequestParam(value = "attachmentName", required = false) String attachmentName) { if (StringUtil.isBlank(attachmentName)) { // 附件名为空,查询附件名 Attachment attachment = attachmentService.getOne(Wrappers.lambdaQuery() @@ -94,7 +95,7 @@ public class FileController extends BladeController { attachmentName = attachmentName.replaceAll(",", "_"); } } - return R.data(fileService.getFileUrl(objectKey, attachmentName, null)); + return FR.data(fileService.getFileUrl(objectKey, attachmentName, null)); } /** @@ -104,9 +105,9 @@ public class FileController extends BladeController { */ @Operation(summary = "获取wps文件预览url", description = "获取wps文件预览url") @GetMapping("/getWpsFilePreviewUrl") - public R getWpsFilePreviewUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId, + public FR getWpsFilePreviewUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId, @NotBlank(message = "附件名称不能为空") @RequestParam(value = "attachmentName", required = false) String attachmentName) { - return R.data(wpsService.getFilePreviewUrl(attachmentId, attachmentName)); + return FR.data(wpsService.getFilePreviewUrl(attachmentId, attachmentName)); } /** @@ -117,41 +118,41 @@ public class FileController extends BladeController { */ @Operation(summary = "获取wps文件编辑url", description = "获取wps文件编辑url") @GetMapping("/getWpsFileEditUrl") - public R getWpsFileEditUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId, + public FR getWpsFileEditUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId, @NotBlank(message = "附件名称不能为空") @RequestParam(value = "attachmentName", required = false) String attachmentName) { - return R.data(wpsService.getWpsFileEditUrl(attachmentId, attachmentName)); + return FR.data(wpsService.getWpsFileEditUrl(attachmentId, attachmentName)); } @Operation(summary = "批量获取文件url", description = "批量获取文件url,参数:objectKey数组") @PostMapping("/getFileUrls") - public R getFileUrls(@RequestBody List objectKeys) { - return R.data(fileService.getFileUrls(objectKeys, null)); + public FR getFileUrls(@RequestBody List objectKeys) { + return FR.data(fileService.getFileUrls(objectKeys, null)); } @ApiLog("OCR识别-识别身份证") @Operation(summary = "识别身份证信息支持正反面", description = "参数:url") @GetMapping("/recognitionIDCard") - public R recognitionIDCard(@RequestParam(value = "url", required = false) String url, - @RequestParam(value = "objectKey", required = false) String objectKey) { + public FR recognitionIDCard(@RequestParam(value = "url", required = false) String url, + @RequestParam(value = "objectKey", required = false) String objectKey) { String imageUrl = StringUtil.isNotBlank(url) ? url : objectKey; if (StringUtil.isBlank(imageUrl)) { - return R.fail("图片地址不能为空"); + return FR.fail("图片地址不能为空"); } if (!imageUrl.startsWith("http://") && !imageUrl.startsWith("https://")) { imageUrl = fileService.getFileUrl(imageUrl, null); } - return R.data(ocrService.recognitionIDCard(List.of(imageUrl))); + return FR.data(ocrService.recognitionIDCard(List.of(imageUrl))); } @SentinelResource("ocr:batchCards") @ApiLog("OCR识别-识别车辆运输凭证(不知道类型)") @Operation(summary = "识别车辆运输凭证,不知道类型", description = "参数:objectKeylist") @PostMapping("/recognitionTransportCertificates") - public R recognitionTransportCertificates( + public FR recognitionTransportCertificates( @RequestBody List attachments, @RequestParam(value = "projectAbbreviation", required = false) String projectAbbreviation, @RequestParam(value = "code", required = false) String code) { - return R.data(ocrConvertService.recognitionTransportCertificate( + return FR.data(ocrConvertService.recognitionTransportCertificate( buildCertificateBatchRecognitionDTO(attachments, projectAbbreviation, code) )); } diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IAttachmentService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IAttachmentService.java index 0a54e01..d423a13 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/service/IAttachmentService.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IAttachmentService.java @@ -26,6 +26,7 @@ package org.springblade.file.service; import com.baomidou.mybatisplus.extension.service.IService; +import org.springblade.core.tool.api.FR; import org.springblade.core.tool.api.R; import org.springblade.file.pojo.entity.Attachment; import org.springblade.file.pojo.vo.AttachmentDetailVO; @@ -49,7 +50,7 @@ public interface IAttachmentService extends IService { * @param fileName 文件名,如果 files只有1个,且fileName不为空,设置文件名为 fileName * @return */ - R> batchOssUpload(MultipartFile[] files, String fileName); + FR> batchOssUpload(MultipartFile[] files, String fileName); /** * 获取附件,多个附件id用逗号分割 diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/AttachmentServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/AttachmentServiceImpl.java index 67bd5fb..4a5195e 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/AttachmentServiceImpl.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/AttachmentServiceImpl.java @@ -32,6 +32,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.api.FR; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.CollectionUtil; import org.springblade.core.tool.utils.SpringUtil; @@ -78,7 +79,7 @@ public class AttachmentServiceImpl extends ServiceImpl> batchOssUpload(MultipartFile[] files, String overWriteFileName){ + public FR> batchOssUpload(MultipartFile[] files, String overWriteFileName){ try { List attachmentList = new ArrayList<>(); // 文件数量为1个,且重写的文件名不为空,使用重写的文件名,给uniapp上传使用,uniapp上传的文件名不是原始文件名 @@ -98,11 +99,11 @@ public class AttachmentServiceImpl extends ServiceImpl + * 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.transport.controller; + +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.api.R; +import org.springblade.transport.ocr.constant.BaiduOcrType; +import org.springblade.transport.ocr.service.IBaiduOcrService; +import org.springblade.transport.pojo.vo.BaiduOcrResultVO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +/** + * 百度 OCR 控制器。 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@Slf4j +@RequestMapping("/baidu-ocr") +@Tag(name = "百度OCR", description = "百度OCR证件识别") +public class BaiduOcrController extends BladeController { + + private final IBaiduOcrService baiduOcrService; + + /** + * 识别上传的图片。 + * + * @param file 图片文件 + * @param type 证件类型:id_card、business_license、vehicle_license、driving_license、road_transport_certificate、general + * @param side 正副面:front或back,适用于身份证、行驶证、驾驶证,默认front + * @return OCR结果 + */ + @PostMapping(value = "/recognize", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ApiOperationSupport(order = 1) + @Operation(summary = "上传图片OCR识别", description = "支持身份证、营业执照、行驶证、驾驶证、道路运输证和通用文字识别") + public R recognize( + @RequestPart("file") MultipartFile file, + @Parameter(description = "OCR证件类型", required = true) @RequestParam String type, + @Parameter(description = "正副面:front或back,适用于身份证、行驶证、驾驶证,默认front") @RequestParam(required = false) String side) { + if (file == null || file.isEmpty()) { + throw new ServiceException("OCR图片不能为空"); + } + BaiduOcrType ocrType = resolveType(type); + if (file.getSize() > ocrType.getMaxRawSize()) { + throw new ServiceException("OCR图片过大,请压缩后重新上传"); + } + try { + return R.data(baiduOcrService.recognize(ocrType, side, file.getBytes())); + } catch (IOException exception) { + log.error("读取OCR图片失败,type={},size={}", ocrType.name(), file.getSize(), exception); + throw new ServiceException("读取OCR图片失败"); + } + } + + /** + * 识别图片地址。 + * + * @param imageUrl 图片地址 + * @param type 证件类型 + * @param side 正副面 + * @return OCR结果 + */ + @PostMapping("/recognize-url") + @ApiOperationSupport(order = 2) + @Operation(summary = "图片地址OCR识别", description = "图片地址必须是百度可访问的HTTP或HTTPS地址") + public R recognizeUrl( + @Parameter(description = "图片地址", required = true) @RequestParam String imageUrl, + @Parameter(description = "OCR证件类型", required = true) @RequestParam String type, + @Parameter(description = "正副面:front或back,适用于身份证、行驶证、驾驶证,默认front") @RequestParam(required = false) String side) { + return R.data(baiduOcrService.recognizeUrl(resolveType(type), side, imageUrl)); + } + + private BaiduOcrType resolveType(String type) { + try { + return BaiduOcrType.from(type); + } catch (IllegalArgumentException exception) { + throw new ServiceException(exception.getMessage()); + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java index 4b8ba6c..412d674 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java @@ -136,6 +136,12 @@ public class ContractManageController extends BladeController { return R.status(contractManageService.startChange(id, changeContent, changeReason)); } + @PostMapping("/submit-change") + @Operation(summary = "提交合同变更") + public R submitChange(@RequestBody ContractManage contractManage) { + return R.status(contractManageService.submitChange(contractManage)); + } + @PostMapping("/terminate") @ApiOperationSupport(order = 11) @Operation(summary = "终止合同", description = "传入id和reason") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceOcrTemplateController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceOcrTemplateController.java new file mode 100644 index 0000000..100dd40 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceOcrTemplateController.java @@ -0,0 +1,98 @@ +/** + * 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.transport.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.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.pojo.entity.InsuranceOcrTemplate; +import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO; +import org.springblade.transport.service.IInsuranceOcrTemplateService; +import org.springblade.transport.wrapper.InsuranceOcrTemplateWrapper; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 保险OCR识别模板控制器。 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "insurance_ocr_template") +@RequestMapping("/insurance-ocr-template") +@Tag(name = "保险OCR识别模板", description = "保险OCR识别模板") +public class InsuranceOcrTemplateController extends BladeController { + + private final IInsuranceOcrTemplateService insuranceOcrTemplateService; + + @GetMapping("/detail") + @ApiOperationSupport(order = 1) + @Operation(summary = "详情", description = "传入id") + public R detail(@Parameter(description = "主键", required = true) @RequestParam Long id) { + InsuranceOcrTemplate insuranceOcrTemplate = insuranceOcrTemplateService.getById(id); + if (insuranceOcrTemplate == null || insuranceOcrTemplate.getIsDeleted() == 1) { + throw new ServiceException("保险OCR识别模板不存在"); + } + return R.data(InsuranceOcrTemplateWrapper.build().entityVO(insuranceOcrTemplate)); + } + + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页", description = "传入insuranceOcrTemplate") + public R> list(InsuranceOcrTemplateVO insuranceOcrTemplate, Query query) { + return R.data(insuranceOcrTemplateService.selectInsuranceOcrTemplatePage(Condition.getPage(query), insuranceOcrTemplate)); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 3) + @Operation(summary = "新增或修改", description = "传入insuranceOcrTemplate") + public R submit(@RequestBody InsuranceOcrTemplate insuranceOcrTemplate) { + return R.status(insuranceOcrTemplateService.submit(insuranceOcrTemplate)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 4) + @Operation(summary = "逻辑删除", description = "传入ids") + public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { + return R.status(insuranceOcrTemplateService.deleteLogic(Func.toLongList(ids))); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java index 6c0d3c3..4dc3153 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java @@ -166,7 +166,7 @@ public class InsuranceRecordController extends BladeController { */ @PostMapping("/recognize") @ApiOperationSupport(order = 8) - @Operation(summary = "OCR识别保单", description = "上传保单图片或PDF") + @Operation(summary = "OCR识别保单", description = "上传保单图片") public R recognize(MultipartFile file, @RequestParam(required = false) String vehicleType, @RequestParam(required = false) String ocrTemplate) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java index 24aea17..dfa2720 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java @@ -35,6 +35,7 @@ import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; +import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; @@ -49,6 +50,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; import java.util.Map; /** @@ -94,8 +96,24 @@ public class ReceivablePayableDetailController extends BladeController { return R.success("更新成功"); } - @GetMapping("/transfer-candidates") + @GetMapping("/update-fee-contracts") @ApiOperationSupport(order = 5) + @Operation(summary = "更新费用可选合同") + public R>> updateFeeContracts( + @RequestParam(required = false) String settlementType) { + return R.data(detailService.updateFeeContracts(settlementType)); + } + + @PostMapping("/adjust-fee") + @ApiOperationSupport(order = 6) + @Operation(summary = "调整费用") + public R adjustFee(@RequestBody ReceivablePayableAdjustFeeRequest request) { + detailService.adjustFee(request); + return R.success("保存成功"); + } + + @GetMapping("/transfer-candidates") + @ApiOperationSupport(order = 7) @Operation(summary = "转结算候选明细") public R>> transferCandidates(Query query, @RequestParam(required = false) String contractName, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceOcrTemplateMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceOcrTemplateMapper.java new file mode 100644 index 0000000..7984fed --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceOcrTemplateMapper.java @@ -0,0 +1,38 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.transport.pojo.entity.InsuranceOcrTemplate; + +/** + * 保险OCR识别模板Mapper接口。 + * + * @author Chill + */ +public interface InsuranceOcrTemplateMapper extends BaseMapper { + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrConfiguration.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrConfiguration.java new file mode 100644 index 0000000..bc3c9de --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrConfiguration.java @@ -0,0 +1,55 @@ +/** + * 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.transport.ocr.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.net.http.HttpClient; + +/** + * 百度 OCR HTTP 客户端配置。 + * + * @author Chill + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(BaiduOcrProperties.class) +public class BaiduOcrConfiguration { + + /** + * 创建百度 OCR HTTP 客户端。 + * + * @param properties 百度 OCR 配置 + * @return HTTP 客户端 + */ + @Bean(name = "baiduOcrHttpClient") + public HttpClient baiduOcrHttpClient(BaiduOcrProperties properties) { + return HttpClient.newBuilder() + .connectTimeout(properties.getConnectTimeout()) + .build(); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrProperties.java new file mode 100644 index 0000000..8cc4529 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/config/BaiduOcrProperties.java @@ -0,0 +1,79 @@ +/** + * 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.transport.ocr.config; + +import lombok.Data; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * 百度 OCR 配置。 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "baidu.ocr") +public class BaiduOcrProperties { + + /** + * 是否启用百度 OCR。 + */ + private boolean enabled = false; + + /** + * 百度智能云应用 API Key。 + */ + @ToString.Exclude + private String apiKey; + + /** + * 百度智能云应用 Secret Key。 + */ + @ToString.Exclude + private String secretKey; + + /** + * 百度 OCR 服务地址。 + */ + private String endpoint = "https://aip.baidubce.com"; + + /** + * 建立百度接口连接的超时时间。 + */ + private Duration connectTimeout = Duration.ofSeconds(5); + + /** + * 百度接口请求超时时间。 + */ + private Duration requestTimeout = Duration.ofSeconds(30); + + /** + * 提前刷新 access_token 的时间。 + */ + private Duration tokenRefreshAdvance = Duration.ofMinutes(1); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/constant/BaiduOcrType.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/constant/BaiduOcrType.java new file mode 100644 index 0000000..8384508 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/constant/BaiduOcrType.java @@ -0,0 +1,100 @@ +/** + * 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.transport.ocr.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Locale; + +/** + * 百度 OCR 支持的证件类型。 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum BaiduOcrType { + + /** 身份证。 */ + ID_CARD("身份证", "/rest/2.0/ocr/v1/idcard", "id_card_side", 8 * 1024 * 1024, 8192), + /** 营业执照。 */ + BUSINESS_LICENSE("营业执照", "/rest/2.0/ocr/v1/business_license", null, 10 * 1024 * 1024, 8192), + /** 行驶证。 */ + VEHICLE_LICENSE("行驶证", "/rest/2.0/ocr/v1/vehicle_license", "vehicle_license_side", 4 * 1024 * 1024, 4096), + /** 驾驶证。 */ + DRIVING_LICENSE("驾驶证", "/rest/2.0/ocr/v1/driving_license", "driving_license_side", 4 * 1024 * 1024, 4096), + /** 道路运输证。 */ + ROAD_TRANSPORT_CERTIFICATE("道路运输证", "/rest/2.0/ocr/v1/road_transport_certificate", null, 4 * 1024 * 1024, 4096), + /** 通用文字识别(标准版)。 */ + GENERAL("通用证件", "/rest/2.0/ocr/v1/general_basic", null, 8 * 1024 * 1024, 4096); + + private final String description; + private final String path; + private final String sideParameter; + private final int maxEncodedSize; + private final int maxDimension; + + /** + * 将请求参数转换为证件类型。 + * + * @param value 类型名称、枚举名或常用别名 + * @return 证件类型 + */ + public static BaiduOcrType from(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("OCR证件类型不能为空"); + } + String normalized = value.trim().replace('-', '_').replace(' ', '_').toUpperCase(Locale.ROOT); + return switch (normalized) { + case "ID_CARD", "IDCARD", "ID" -> ID_CARD; + case "BUSINESS_LICENSE", "BUSINESSLICENSE", "LICENSE", "营业执照" -> BUSINESS_LICENSE; + case "VEHICLE_LICENSE", "VEHICLELICENSE", "DRIVING_VEHICLE", "行驶证" -> VEHICLE_LICENSE; + case "DRIVING_LICENSE", "DRIVINGLICENSE", "驾驶证" -> DRIVING_LICENSE; + case "ROAD_TRANSPORT_CERTIFICATE", "ROADTRANSPORTCERTIFICATE", "ROAD_TRANSPORT", "道路运输证" -> ROAD_TRANSPORT_CERTIFICATE; + case "GENERAL", "GENERAL_BASIC", "COMMON", "COMMON_CARD", "通用证件", "通用文字识别" -> GENERAL; + default -> throw new IllegalArgumentException("不支持的OCR证件类型:" + value); + }; + } + + /** + * 判断该类型是否支持正副面参数。 + * + * @return 是否支持正副面 + */ + public boolean supportsSide() { + return sideParameter != null; + } + + /** + * 获取上传图片原始大小的理论上限。 + * + * @return 原始大小上限 + */ + public long getMaxRawSize() { + return (long) maxEncodedSize * 3 / 4; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/IBaiduOcrService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/IBaiduOcrService.java new file mode 100644 index 0000000..5b1c1c0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/IBaiduOcrService.java @@ -0,0 +1,57 @@ +/** + * 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.transport.ocr.service; + +import org.springblade.transport.ocr.constant.BaiduOcrType; +import org.springblade.transport.pojo.vo.BaiduOcrResultVO; + +/** + * 百度 OCR 服务。 + * + * @author Chill + */ +public interface IBaiduOcrService { + + /** + * 识别上传的图片。 + * + * @param type 证件类型 + * @param side 正副面 + * @param image 图片二进制 + * @return 识别结果 + */ + BaiduOcrResultVO recognize(BaiduOcrType type, String side, byte[] image); + + /** + * 识别图片地址。 + * + * @param type 证件类型 + * @param side 正副面 + * @param imageUrl 图片地址 + * @return 识别结果 + */ + BaiduOcrResultVO recognizeUrl(BaiduOcrType type, String side, String imageUrl); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/impl/BaiduOcrServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/impl/BaiduOcrServiceImpl.java new file mode 100644 index 0000000..3a60627 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/ocr/service/impl/BaiduOcrServiceImpl.java @@ -0,0 +1,356 @@ +/** + * 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.transport.ocr.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.transport.ocr.config.BaiduOcrProperties; +import org.springblade.transport.ocr.constant.BaiduOcrType; +import org.springblade.transport.ocr.service.IBaiduOcrService; +import org.springblade.transport.pojo.vo.BaiduOcrResultVO; +import org.springframework.stereotype.Service; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 百度 OCR 服务实现。 + * + * @author Chill + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class BaiduOcrServiceImpl implements IBaiduOcrService { + + private static final String TOKEN_PATH = "/oauth/2.0/token"; + private static final String TOKEN_GRANT_TYPE = "client_credentials"; + private static final int ACCESS_TOKEN_INVALID = 110; + private static final int ACCESS_TOKEN_EXPIRED = 111; + private static final int MIN_IMAGE_DIMENSION = 15; + private static final Set SUPPORTED_IMAGE_FORMATS = Set.of("JPEG", "JPG", "PNG", "BMP"); + private static final TypeReference> RESULT_TYPE = new TypeReference<>() { + }; + + private final BaiduOcrProperties properties; + private final ObjectMapper objectMapper; + private final HttpClient httpClient; + private final Object tokenMonitor = new Object(); + + private volatile AccessToken accessToken; + + @Override + public BaiduOcrResultVO recognize(BaiduOcrType type, String side, byte[] image) { + validateType(type); + if (image == null || image.length == 0) { + throw new ServiceException("OCR图片不能为空"); + } + if (image.length > type.getMaxRawSize()) { + throw new ServiceException("OCR图片过大,请压缩后重新上传"); + } + validateImage(type, image); + String imageBase64 = Base64.getEncoder().encodeToString(image); + String encodedImage = URLEncoder.encode(imageBase64, StandardCharsets.UTF_8); + if (encodedImage.length() > type.getMaxEncodedSize()) { + throw new ServiceException(type.getDescription() + "图片经Base64和URL编码后不能超过" + + (type.getMaxEncodedSize() / 1024 / 1024) + "M"); + } + Map form = new LinkedHashMap<>(); + form.put("image", imageBase64); + return request(type, side, form); + } + + @Override + public BaiduOcrResultVO recognizeUrl(BaiduOcrType type, String side, String imageUrl) { + validateType(type); + if (StringUtil.isBlank(imageUrl)) { + throw new ServiceException("OCR图片地址不能为空"); + } + String normalizedUrl = imageUrl.trim(); + validateImageUrl(normalizedUrl); + Map form = new LinkedHashMap<>(); + form.put("url", normalizedUrl); + return request(type, side, form); + } + + private BaiduOcrResultVO request(BaiduOcrType type, String side, Map form) { + validateType(type); + if (!properties.isEnabled()) { + throw new ServiceException("百度OCR服务未启用"); + } + if (StringUtil.isBlank(properties.getApiKey()) || StringUtil.isBlank(properties.getSecretKey())) { + throw new ServiceException("百度OCR的API Key和Secret Key未配置"); + } + String normalizedSide = normalizeSide(type, side); + if (normalizedSide != null) { + form.put(type.getSideParameter(), normalizedSide); + } + JsonNode result = sendOcrRequest(type, form); + int errorCode = result.path("error_code").asInt(0); + if (errorCode != 0) { + String message = result.path("error_msg").asText("未知错误"); + log.warn("百度OCR识别失败,type={},side={},errorCode={},logId={}", + type.name(), normalizedSide, errorCode, result.path("log_id").asText("")); + throw new ServiceException("百度OCR识别失败(" + errorCode + "):" + message); + } + BaiduOcrResultVO response = new BaiduOcrResultVO(); + response.setType(type.name()); + response.setSide(normalizedSide); + response.setResult(objectMapper.convertValue(result, RESULT_TYPE)); + return response; + } + + private JsonNode sendOcrRequest(BaiduOcrType type, Map form) { + String token = getAccessToken(); + JsonNode result = executeOcrRequest(type, form, token); + if (isAccessTokenInvalid(result)) { + invalidateAccessToken(token); + log.warn("百度OCR access_token 已失效,重新获取后重试,type={},errorCode={}", + type.name(), result.path("error_code").asInt()); + result = executeOcrRequest(type, form, getAccessToken()); + } + return result; + } + + private JsonNode executeOcrRequest(BaiduOcrType type, Map form, String token) { + try { + URI requestUri = URI.create(buildEndpoint(type.getPath()) + "?access_token=" + + URLEncoder.encode(token, StandardCharsets.UTF_8)); + HttpRequest request = HttpRequest.newBuilder(requestUri) + .timeout(properties.getRequestTimeout()) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(toFormBody(form), StandardCharsets.UTF_8)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + log.error("百度OCR接口响应异常,type={}, status={}", type.name(), response.statusCode()); + throw new ServiceException("百度OCR接口调用失败,HTTP状态码:" + response.statusCode()); + } + JsonNode result = objectMapper.readTree(response.body()); + if (result == null || !result.isObject()) { + throw new ServiceException("百度OCR响应格式异常"); + } + return result; + } catch (ServiceException exception) { + throw exception; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + log.error("调用百度OCR接口被中断,type={}", type.name(), exception); + throw new ServiceException("百度OCR接口调用被中断"); + } catch (IllegalArgumentException exception) { + log.error("百度OCR接口地址配置不正确,type={}", type.name()); + throw new ServiceException("百度OCR接口地址配置不正确"); + } catch (IOException exception) { + log.error("调用百度OCR接口失败,type={}", type.name(), exception); + throw new ServiceException("百度OCR接口调用失败"); + } + } + + private boolean isAccessTokenInvalid(JsonNode result) { + int errorCode = result.path("error_code").asInt(0); + return errorCode == ACCESS_TOKEN_INVALID || errorCode == ACCESS_TOKEN_EXPIRED; + } + + private void invalidateAccessToken(String rejectedToken) { + synchronized (tokenMonitor) { + if (accessToken != null && Objects.equals(accessToken.value(), rejectedToken)) { + accessToken = null; + } + } + } + + private String getAccessToken() { + AccessToken currentToken = accessToken; + if (currentToken != null && currentToken.isValid(properties.getTokenRefreshAdvance())) { + return currentToken.value(); + } + synchronized (tokenMonitor) { + currentToken = accessToken; + if (currentToken != null && currentToken.isValid(properties.getTokenRefreshAdvance())) { + return currentToken.value(); + } + return requestAccessToken(); + } + } + + private String requestAccessToken() { + try { + String query = "grant_type=" + URLEncoder.encode(TOKEN_GRANT_TYPE, StandardCharsets.UTF_8) + + "&client_id=" + URLEncoder.encode(properties.getApiKey(), StandardCharsets.UTF_8) + + "&client_secret=" + URLEncoder.encode(properties.getSecretKey(), StandardCharsets.UTF_8); + HttpRequest request = HttpRequest.newBuilder(URI.create(buildEndpoint(TOKEN_PATH) + "?" + query)) + .timeout(properties.getRequestTimeout()) + .header("Accept", "application/json") + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + JsonNode result = objectMapper.readTree(response.body()); + if (result == null || !result.isObject()) { + throw new ServiceException("百度OCR鉴权响应格式异常"); + } + String token = result.path("access_token").asText(null); + long expiresIn = result.path("expires_in").asLong(0); + if (response.statusCode() < 200 || response.statusCode() >= 300 || StringUtil.isBlank(token) || expiresIn <= 0) { + String error = result.path("error").asText(""); + String message = result.path("error_description").asText("未知错误"); + log.error("百度OCR鉴权失败,status={},error={}", response.statusCode(), error); + throw new ServiceException("百度OCR鉴权失败:" + message); + } + accessToken = new AccessToken(token, Instant.now().plusSeconds(expiresIn)); + return token; + } catch (ServiceException exception) { + throw exception; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new ServiceException("百度OCR鉴权请求被中断"); + } catch (IllegalArgumentException exception) { + log.error("百度OCR鉴权地址配置不正确"); + throw new ServiceException("百度OCR鉴权地址配置不正确"); + } catch (IOException exception) { + log.error("获取百度OCR access_token 失败", exception); + throw new ServiceException("百度OCR鉴权失败"); + } + } + + private String normalizeSide(BaiduOcrType type, String side) { + if (StringUtil.isBlank(side)) { + return type.supportsSide() ? "front" : null; + } + if (!type.supportsSide()) { + throw new ServiceException(type.getDescription() + "不支持正副面参数"); + } + String normalized = side.trim().toLowerCase(Locale.ROOT); + if ("front".equals(normalized) || "main".equals(normalized) || "正面".equals(normalized) || "主页".equals(normalized)) { + return "front"; + } + if ("back".equals(normalized) || "side".equals(normalized) || "副面".equals(normalized) || "副页".equals(normalized) || "反面".equals(normalized)) { + return "back"; + } + throw new ServiceException("OCR证件面参数只能是front或back"); + } + + private void validateType(BaiduOcrType type) { + if (type == null) { + throw new ServiceException("OCR证件类型不能为空"); + } + } + + private void validateImageUrl(String imageUrl) { + if (imageUrl.getBytes(StandardCharsets.UTF_8).length > 1024) { + throw new ServiceException("OCR图片地址长度不能超过1024字节"); + } + try { + URI imageUri = URI.create(imageUrl); + String scheme = imageUri.getScheme(); + if (StringUtil.isBlank(scheme) || StringUtil.isBlank(imageUri.getHost()) + || imageUri.getUserInfo() != null + || !("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) { + throw new ServiceException("OCR图片地址必须是有效的HTTP或HTTPS地址"); + } + } catch (IllegalArgumentException exception) { + throw new ServiceException("OCR图片地址格式不正确"); + } + } + + private void validateImage(BaiduOcrType type, byte[] image) { + try (ImageInputStream imageInputStream = ImageIO.createImageInputStream(new ByteArrayInputStream(image))) { + if (imageInputStream == null) { + throw new ServiceException("OCR图片格式不正确"); + } + Iterator imageReaders = ImageIO.getImageReaders(imageInputStream); + if (!imageReaders.hasNext()) { + throw new ServiceException("OCR仅支持JPG、JPEG、PNG、BMP图片"); + } + ImageReader imageReader = imageReaders.next(); + try { + imageReader.setInput(imageInputStream, true, true); + String formatName = imageReader.getFormatName().toUpperCase(Locale.ROOT); + if (!SUPPORTED_IMAGE_FORMATS.contains(formatName)) { + throw new ServiceException("OCR仅支持JPG、JPEG、PNG、BMP图片"); + } + int width = imageReader.getWidth(0); + int height = imageReader.getHeight(0); + if (Math.min(width, height) < MIN_IMAGE_DIMENSION || Math.max(width, height) > type.getMaxDimension()) { + throw new ServiceException(type.getDescription() + "图片最短边不能小于15px,最长边不能超过" + + type.getMaxDimension() + "px"); + } + } finally { + imageReader.dispose(); + } + } catch (ServiceException exception) { + throw exception; + } catch (IOException exception) { + log.error("解析OCR图片失败,type={},size={}", type.name(), image.length, exception); + throw new ServiceException("OCR图片格式不正确"); + } + } + + private String toFormBody(Map form) { + return form.entrySet().stream() + .map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" + + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) + .reduce((left, right) -> left + "&" + right) + .orElse(""); + } + + private String buildEndpoint(String path) { + String endpoint = properties.getEndpoint(); + if (StringUtil.isBlank(endpoint)) { + throw new ServiceException("百度OCR服务地址未配置"); + } + return endpoint.replaceAll("/+$", "") + path; + } + + private record AccessToken(String value, Instant expiresAt) { + private boolean isValid(Duration advance) { + Duration refreshAdvance = advance == null || advance.isNegative() ? Duration.ZERO : advance; + return StringUtil.isNotBlank(value) && expiresAt.isAfter(Instant.now().plus(refreshAdvance)); + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java index 801ce23..a1299d8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java @@ -48,6 +48,7 @@ public interface IContractManageService extends BaseService { boolean reject(Long id); boolean withdraw(Long id); boolean startChange(Long id, String changeContent, String changeReason); + boolean submitChange(ContractManage contractManage); boolean updateAttachments(ContractManage contractManage); boolean terminate(Long id, String reason); boolean removeDraft(String ids); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInsuranceOcrTemplateService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInsuranceOcrTemplateService.java new file mode 100644 index 0000000..937c086 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInsuranceOcrTemplateService.java @@ -0,0 +1,57 @@ +/** + * 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.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.entity.InsuranceOcrTemplate; +import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO; + +/** + * 保险OCR识别模板服务类。 + * + * @author Chill + */ +public interface IInsuranceOcrTemplateService extends BaseService { + + /** + * 分页查询模板。 + * + * @param page 分页参数 + * @param insuranceOcrTemplate 查询条件 + * @return 模板分页 + */ + IPage selectInsuranceOcrTemplatePage(IPage page, InsuranceOcrTemplateVO insuranceOcrTemplate); + + /** + * 保存模板。 + * + * @param insuranceOcrTemplate 模板 + * @return 是否成功 + */ + boolean submit(InsuranceOcrTemplate insuranceOcrTemplate); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index f085d7c..a500178 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -24,6 +24,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; @@ -32,6 +33,7 @@ import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; +import java.util.List; import java.util.Map; /** @@ -49,6 +51,10 @@ public interface IReceivablePayableDetailService extends BaseService> updateFeeContracts(String settlementType); + + void adjustFee(ReceivablePayableAdjustFeeRequest request); + void transferSettlement(ReceivablePayableTransferRequest request); IPage> transferCandidates(IPage page, String contractName, String batchNo, @@ -59,4 +65,7 @@ public interface IReceivablePayableDetailService extends BaseService page, ReceivablePayableGenerateRequest request); void generateFee(ReceivablePayableGenerateRequest request); + + /** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */ + void generateForCompletedWaybills(List waybillIds); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index d61c6ce..21eefe7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -44,6 +44,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -205,6 +206,25 @@ public class ContractManageServiceImpl extends BaseServiceImpl rows) || rows.isEmpty()) return; + BigDecimal total = BigDecimal.ZERO; + for (Object row : rows) { + if (row instanceof Map values && values.get("ratioLimit") != null + && !String.valueOf(values.get("ratioLimit")).isBlank()) { + total = total.add(new BigDecimal(String.valueOf(values.get("ratioLimit")))); + } + } + if (total.compareTo(BigDecimal.valueOf(100)) != 0) { + throw new ServiceException("付款比例上限合计必须等于100%"); + } + } catch (ServiceException exception) { + throw exception; + } catch (Exception exception) { + throw new ServiceException("付款比例设置格式不正确"); + } } private void validateContractNameUnique(ContractManage contractManage) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceOcrTemplateServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceOcrTemplateServiceImpl.java new file mode 100644 index 0000000..92b83f5 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceOcrTemplateServiceImpl.java @@ -0,0 +1,154 @@ +/** + * 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.transport.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.transport.mapper.InsuranceOcrTemplateMapper; +import org.springblade.transport.pojo.entity.InsuranceOcrTemplate; +import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO; +import org.springblade.transport.service.IInsuranceOcrTemplateService; +import org.springblade.transport.wrapper.InsuranceOcrTemplateWrapper; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * 保险OCR识别模板服务实现类。 + * + * @author Chill + */ +@Service +public class InsuranceOcrTemplateServiceImpl extends BaseServiceImpl implements IInsuranceOcrTemplateService { + + private static final int NAME_MAX_LENGTH = 100; + private static final int MAPPING_VALUE_MAX_LENGTH = 100; + private static final List INSURANCE_FIELD_KEYS = List.of( + "保险类型", "保单号", "开始日期", "结束日期", "保额", "保费", "发票号", "开票日期", "备注" + ); + + @Override + public IPage selectInsuranceOcrTemplatePage(IPage page, InsuranceOcrTemplateVO insuranceOcrTemplate) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(InsuranceOcrTemplate::getIsDeleted, 0) + .like(StringUtil.isNotBlank(insuranceOcrTemplate.getName()), InsuranceOcrTemplate::getName, insuranceOcrTemplate.getName()) + .orderByDesc(InsuranceOcrTemplate::getCreateTime); + return InsuranceOcrTemplateWrapper.build().pageVO(page(page, queryWrapper)); + } + + @Override + public boolean submit(InsuranceOcrTemplate insuranceOcrTemplate) { + boolean created = insuranceOcrTemplate.getId() == null; + if (!created) { + InsuranceOcrTemplate oldTemplate = getById(insuranceOcrTemplate.getId()); + if (oldTemplate == null || oldTemplate.getIsDeleted() == 1) { + throw new ServiceException("保险OCR识别模板不存在"); + } + insuranceOcrTemplate.setTenantId(oldTemplate.getTenantId()); + } + insuranceOcrTemplate.setName(trimToNull(insuranceOcrTemplate.getName())); + insuranceOcrTemplate.setMappingConfig(trimToNull(insuranceOcrTemplate.getMappingConfig())); + if (created && insuranceOcrTemplate.getStatus() == null) { + insuranceOcrTemplate.setStatus(1); + } + validate(insuranceOcrTemplate); + return saveOrUpdate(insuranceOcrTemplate); + } + + private void validate(InsuranceOcrTemplate insuranceOcrTemplate) { + if (StringUtil.isBlank(insuranceOcrTemplate.getName())) { + throw new ServiceException("模板名称不能为空"); + } + if (insuranceOcrTemplate.getName().length() > NAME_MAX_LENGTH) { + throw new ServiceException("模板名称不能超过100个字符"); + } + Long nameCount = count(Wrappers.lambdaQuery() + .eq(InsuranceOcrTemplate::getIsDeleted, 0) + .eq(InsuranceOcrTemplate::getName, insuranceOcrTemplate.getName()) + .ne(insuranceOcrTemplate.getId() != null, InsuranceOcrTemplate::getId, insuranceOcrTemplate.getId())); + if (nameCount > 0) { + throw new ServiceException("模板名称已存在"); + } + validateMappingConfig(insuranceOcrTemplate.getMappingConfig()); + } + + private void validateMappingConfig(String mappingConfig) { + if (StringUtil.isBlank(mappingConfig)) { + throw new ServiceException("字段映射配置不能为空"); + } + JSONArray mappingArray; + try { + mappingArray = JSON.parseArray(mappingConfig); + } catch (Exception exception) { + throw new ServiceException("字段映射配置格式不正确"); + } + if (mappingArray == null || mappingArray.size() != INSURANCE_FIELD_KEYS.size()) { + throw new ServiceException("字段映射配置必须包含全部保险字段"); + } + Set mappingKeySet = new LinkedHashSet<>(); + boolean hasMappingValue = false; + for (Object item : mappingArray) { + if (!(item instanceof JSONObject mapping)) { + throw new ServiceException("字段映射配置格式不正确"); + } + String key = trimToNull(mapping.getString("key")); + String value = trimToNull(mapping.getString("value")); + if (!INSURANCE_FIELD_KEYS.contains(key)) { + throw new ServiceException("字段映射包含不支持的键名"); + } + if (!mappingKeySet.add(key)) { + throw new ServiceException("字段映射键名不能重复"); + } + if (value != null && value.length() > MAPPING_VALUE_MAX_LENGTH) { + throw new ServiceException("字段映射值不能超过100个字符"); + } + hasMappingValue = hasMappingValue || value != null; + } + if (!mappingKeySet.containsAll(INSURANCE_FIELD_KEYS)) { + throw new ServiceException("字段映射配置必须包含全部保险字段"); + } + if (!hasMappingValue) { + throw new ServiceException("请至少填写一个字段映射值"); + } + } + + private String trimToNull(String value) { + if (value == null || value.isBlank()) { + return null; + } + return value.trim(); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java index b262ddc..a6efd6e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java @@ -25,9 +25,13 @@ */ package org.springblade.transport.service.impl; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.utils.BeanUtil; @@ -36,18 +40,30 @@ import org.springblade.system.cache.UserCache; import org.springblade.transport.excel.InsuranceRecordExcel; import org.springblade.transport.excel.InsuranceRecordExportExcel; import org.springblade.transport.mapper.InsuranceRecordMapper; +import org.springblade.transport.ocr.constant.BaiduOcrType; +import org.springblade.transport.ocr.service.IBaiduOcrService; import org.springblade.transport.pojo.entity.InsuranceRecord; +import org.springblade.transport.pojo.entity.InsuranceOcrTemplate; +import org.springblade.transport.pojo.vo.BaiduOcrResultVO; import org.springblade.transport.pojo.vo.InsuranceRecordVO; +import org.springblade.transport.service.IInsuranceOcrTemplateService; import org.springblade.transport.service.IInsuranceRecordService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * 保险记录 服务实现类 @@ -55,6 +71,7 @@ import java.util.Set; * @author Chill */ @Service +@RequiredArgsConstructor public class InsuranceRecordServiceImpl extends BaseServiceImpl implements IInsuranceRecordService { private static final int VEHICLE_NO_MAX_LENGTH = 50; @@ -64,7 +81,11 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl SUPPORT_FILE_TYPES = Set.of("jpg", "jpeg", "png", "pdf"); + private static final Set SUPPORT_FILE_TYPES = Set.of("jpg", "jpeg", "png", "bmp"); + private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?"); + + private final IBaiduOcrService baiduOcrService; + private final IInsuranceOcrTemplateService insuranceOcrTemplateService; @Override public IPage selectInsuranceRecordPage(IPage page, InsuranceRecordVO insuranceRecord) { @@ -116,10 +137,131 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpllambdaQuery() + .eq(InsuranceOcrTemplate::getIsDeleted, 0) + .eq(InsuranceOcrTemplate::getStatus, 1) + .eq(InsuranceOcrTemplate::getName, templateName)); + if (insuranceOcrTemplate == null) { + throw new ServiceException("OCR识别模板不存在或已停用"); + } InsuranceRecord insuranceRecord = new InsuranceRecord(); insuranceRecord.setVehicleType(normalizeVehicleType(vehicleType)); - insuranceRecord.setOcrTemplate(trimToNull(ocrTemplate)); - throw new ServiceException("当前未配置OCR识别服务,请手动填写保单信息"); + insuranceRecord.setOcrTemplate(templateName); + try { + BaiduOcrResultVO ocrResult = baiduOcrService.recognize(BaiduOcrType.GENERAL, null, file.getBytes()); + applyTemplateMapping(insuranceRecord, insuranceOcrTemplate.getMappingConfig(), ocrResult.getResult()); + return insuranceRecord; + } catch (IOException exception) { + throw new ServiceException("读取保单图片失败"); + } + } + + private void applyTemplateMapping(InsuranceRecord insuranceRecord, String mappingConfig, Map ocrResult) { + Map mappingMap = parseMappingConfig(mappingConfig); + List words = extractOcrWords(ocrResult); + insuranceRecord.setInsuranceType(findMappedValue(words, mappingMap.get("保险类型"))); + insuranceRecord.setPolicyNo(findMappedValue(words, mappingMap.get("保单号"))); + insuranceRecord.setStartDate(parseDate(findMappedValue(words, mappingMap.get("开始日期")))); + insuranceRecord.setEndDate(parseDate(findMappedValue(words, mappingMap.get("结束日期")))); + insuranceRecord.setInsuredAmount(parseAmount(findMappedValue(words, mappingMap.get("保额")))); + insuranceRecord.setPremium(parseAmount(findMappedValue(words, mappingMap.get("保费")))); + insuranceRecord.setInvoiceNo(findMappedValue(words, mappingMap.get("发票号"))); + insuranceRecord.setInvoiceDate(parseDate(findMappedValue(words, mappingMap.get("开票日期")))); + insuranceRecord.setRemark(findMappedValue(words, mappingMap.get("备注"))); + } + + private Map parseMappingConfig(String mappingConfig) { + try { + JSONArray mappingArray = JSON.parseArray(mappingConfig); + Map mappingMap = new LinkedHashMap<>(); + for (Object item : mappingArray) { + if (item instanceof JSONObject mapping) { + String key = trimToNull(mapping.getString("key")); + String value = trimToNull(mapping.getString("value")); + if (key != null && value != null) { + mappingMap.put(key, value); + } + } + } + return mappingMap; + } catch (Exception exception) { + throw new ServiceException("OCR识别模板字段映射配置格式不正确"); + } + } + + private List extractOcrWords(Map ocrResult) { + if (ocrResult == null || ocrResult.isEmpty()) { + return List.of(); + } + Object wordsResult = ocrResult.get("words_result"); + if (wordsResult instanceof Collection wordCollection) { + return wordCollection.stream().map(this::extractWord).filter(Objects::nonNull).toList(); + } + if (wordsResult instanceof Map wordMap) { + return wordMap.values().stream().map(this::extractWord).filter(Objects::nonNull).toList(); + } + return List.of(); + } + + private String extractWord(Object wordItem) { + if (wordItem instanceof Map wordMap) { + Object words = wordMap.get("words"); + return words == null ? null : trimToNull(String.valueOf(words)); + } + return wordItem == null ? null : trimToNull(String.valueOf(wordItem)); + } + + private String findMappedValue(List words, String mappingValue) { + if (mappingValue == null || words.isEmpty()) { + return null; + } + Pattern standalonePattern = Pattern.compile("^" + Pattern.quote(mappingValue) + "\\s*[::]?$"); + Pattern inlinePattern = Pattern.compile(Pattern.quote(mappingValue) + "\\s*[::]?\\s*(.+)$"); + for (int index = 0; index < words.size(); index++) { + String word = words.get(index).trim(); + Matcher inlineMatcher = inlinePattern.matcher(word); + if (inlineMatcher.find()) { + return trimToNull(inlineMatcher.group(1)); + } + if (standalonePattern.matcher(word).matches() && index + 1 < words.size()) { + return trimToNull(words.get(index + 1)); + } + } + return null; + } + + private LocalDate parseDate(String value) { + if (value == null) { + return null; + } + Matcher matcher = DATE_PATTERN.matcher(value); + if (!matcher.find()) { + return null; + } + try { + return LocalDate.of(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3))); + } catch (RuntimeException exception) { + return null; + } + } + + private BigDecimal parseAmount(String value) { + if (value == null) { + return null; + } + String amount = value.replaceAll("[^\\d.]", ""); + if (amount.isBlank()) { + return null; + } + try { + return new BigDecimal(amount); + } catch (NumberFormatException exception) { + return null; + } } private void prepare(InsuranceRecord insuranceRecord) { @@ -186,7 +328,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl implements IReceivablePayableDetailService { @@ -82,15 +90,18 @@ public class ReceivablePayableDetailServiceImpl private final ReceivablePayableChangeRecordMapper changeRecordMapper; private final IWaybillService waybillService; private final IContractManageService contractManageService; + private final ICommonAddressService commonAddressService; public ReceivablePayableDetailServiceImpl(ReceivablePayableCargoFeeMapper cargoFeeMapper, - ReceivablePayableChangeRecordMapper changeRecordMapper, - IWaybillService waybillService, - IContractManageService contractManageService) { + ReceivablePayableChangeRecordMapper changeRecordMapper, + IWaybillService waybillService, + IContractManageService contractManageService, + ICommonAddressService commonAddressService) { this.cargoFeeMapper = cargoFeeMapper; this.changeRecordMapper = changeRecordMapper; this.waybillService = waybillService; this.contractManageService = contractManageService; + this.commonAddressService = commonAddressService; } @Override @@ -105,7 +116,11 @@ public class ReceivablePayableDetailServiceImpl .eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) .eq(ReceivablePayableCargoFee::getIsDeleted, 0) .orderByAsc(ReceivablePayableCargoFee::getCreateTime)); - return buildFeeDetail(rows); + ReceivablePayableFeeDetailVO result = buildFeeDetail(rows); + LinkedHashSet feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); + feeItemNames.addAll(result.getFeeItemNames()); + result.setFeeItemNames(new ArrayList<>(feeItemNames)); + return result; } @Override @@ -125,11 +140,11 @@ public class ReceivablePayableDetailServiceImpl @Transactional(rollbackFor = Exception.class) public void updateFee(ReceivablePayableUpdateFeeRequest request) { if (Boolean.TRUE.equals(request.getCloseOnly())) { - closeDetails(request.getIds()); + closeDetails(request.getIds(), request.getSettlementType()); return; } - if (Func.isEmpty(request.getIds()) && Func.isEmpty(request.getContractId())) { - throw new ServiceException("请选择需要更新的费用明细或合同"); + if (Func.isEmpty(request.getContractId())) { + throw new ServiceException("请选择需要更新费用的合同"); } List details = list(buildUpdateQuery(request)); if (Func.isEmpty(details)) { @@ -140,12 +155,107 @@ public class ReceivablePayableDetailServiceImpl continue; } BigDecimal before = money(detail.getTotalAmount()); - rebuildDetailFee(detail); + rebuildDetailFee(detail, request.getBillingPlanId()); + if (request.getAdjustAmount() != null && request.getAdjustAmount().compareTo(BigDecimal.ZERO) != 0) { + applyManualAdjustment(detail, request.getAdjustAmount(), request.getAdjustFeeItem(), request.getAdjustReason()); + } saveChangeRecord(detail, "【费用合计】从[" + formatMoney(before) + "]调整为[" + formatMoney(detail.getTotalAmount()) + "]", request.getAdjustReason()); } } + @Override + public List> updateFeeContracts(String settlementType) { + List contractIds = list(Wrappers.lambdaQuery() + .select(ReceivablePayableDetail::getContractId) + .eq(ReceivablePayableDetail::getIsDeleted, 0) + .eq(ReceivablePayableDetail::getSettlementStatus, "pending") + .eq(Func.isNotEmpty(settlementType), ReceivablePayableDetail::getSettlementType, + settlementType(settlementType)) + .isNotNull(ReceivablePayableDetail::getContractId) + .groupBy(ReceivablePayableDetail::getContractId)) + .stream().map(ReceivablePayableDetail::getContractId).toList(); + if (Func.isEmpty(contractIds)) { + return List.of(); + } + Map contracts = contractManageService.listByIds(contractIds).stream() + .collect(java.util.stream.Collectors.toMap(ContractManage::getId, contract -> contract)); + return contractIds.stream().map(contracts::get).filter(Objects::nonNull).map(contract -> { + Map item = new LinkedHashMap<>(); + item.put("id", contract.getId()); + item.put("contractNo", contract.getContractNo()); + item.put("contractName", contract.getContractName()); + item.put("billingPlanJson", contract.getBillingPlanJson()); + return item; + }).toList(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void adjustFee(ReceivablePayableAdjustFeeRequest request) { + if (request == null || request.getDetailId() == null || Func.isEmpty(request.getRows())) { + throw new ServiceException("费用调整数据不能为空"); + } + ReceivablePayableDetail detail = getExisting(request.getDetailId()); + if (!"pending".equals(detail.getSettlementStatus())) { + throw new ServiceException("仅待结算明细允许调整"); + } + List existingRows = cargoFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0)); + Map existingMap = existingRows.stream() + .collect(java.util.stream.Collectors.toMap(ReceivablePayableCargoFee::getId, row -> row)); + if (request.getRows().size() != existingRows.size()) { + throw new ServiceException("费用调整行数据不完整"); + } + Set allowedFeeItems = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); + existingRows.forEach(row -> allowedFeeItems.addAll(parseMap(row.getFeeItemsJson()).keySet())); + List changes = new ArrayList<>(); + for (ReceivablePayableAdjustFeeRequest.AdjustRow adjusted : request.getRows()) { + ReceivablePayableCargoFee existing = existingMap.get(adjusted.getId()); + if (existing == null) { + throw new ServiceException("存在无效的费用调整行"); + } + validateNonNegative(adjusted.getTransportQuantity(), "运输量"); + validateNonNegative(adjusted.getMileage(), "里程"); + validateNonNegative(adjusted.getFreightAmount(), "运输费"); + Map feeItems = new LinkedHashMap<>(); + if (adjusted.getFeeItems() != null) { + adjusted.getFeeItems().forEach((name, amount) -> { + if (!allowedFeeItems.contains(name)) { + throw new ServiceException("费用项目不存在:" + name); + } + validateNonNegative(amount, name); + feeItems.put(name, money(amount)); + }); + } + appendChange(changes, "计费数量", existing.getTransportQuantity(), adjusted.getTransportQuantity()); + appendChange(changes, "里程", existing.getMileage(), adjusted.getMileage()); + appendChange(changes, "运输费", existing.getFreightAmount(), adjusted.getFreightAmount()); + Map oldFeeItems = parseMap(existing.getFeeItemsJson()); + for (String name : allowedFeeItems) { + appendChange(changes, name, decimal(oldFeeItems.get(name)), money(feeItems.get(name))); + } + BigDecimal freightAmount = money(adjusted.getFreightAmount()); + BigDecimal afterAmount = adjustedAfterAmount(freightAmount, feeItems); + existing.setTransportQuantity(money(adjusted.getTransportQuantity())); + existing.setMileage(money(adjusted.getMileage())); + existing.setFreightAmount(freightAmount); + existing.setFeeItemsJson(JsonUtil.toJson(feeItems)); + existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount()))); + existing.setAfterAmount(afterAmount); + cargoFeeMapper.updateById(existing); + } + if (changes.isEmpty()) { + throw new ServiceException("未修改任何费用数据"); + } + for (int i = 0; i < changes.size(); i++) { + saveChangeRecord(detail, changes.get(i), request.getAdjustReason(), String.format("%04d", i + 1)); + } + refreshAdjustedDetail(detail, existingRows); + } + @Override @Transactional(rollbackFor = Exception.class) public void transferSettlement(ReceivablePayableTransferRequest request) { @@ -203,11 +313,10 @@ public class ReceivablePayableDetailServiceImpl public ReceivablePayableFeeDetailVO generatePreview(IPage page, ReceivablePayableGenerateRequest request) { validateGenerateRequest(request, true); List waybills = waybillService.list(buildWaybillQuery(request)); + ContractManage contract = contractManageService.getById(request.getContractId()); List fees = waybills.stream() - .skip((page.getCurrent() - 1) * page.getSize()) - .limit(page.getSize()) - .map(waybill -> buildCargoFee(null, waybill)) - .toList(); + .skip((page.getCurrent() - 1) * page.getSize()).limit(page.getSize()) + .flatMap(waybill -> calculatedFees(waybill, contract, request.getBillingPlanId()).stream()).toList(); ReceivablePayableFeeDetailVO vo = buildFeeDetail(fees); vo.setTotal((long) waybills.size()); return vo; @@ -223,16 +332,123 @@ public class ReceivablePayableDetailServiceImpl } ContractManage contract = contractManageService.getById(request.getContractId()); for (Waybill waybill : waybills) { - if (existsByWaybill(waybill.getId())) { + if (existsByWaybill(waybill.getId(), settlementType(request.getSettlementType()))) { continue; } - ReceivablePayableDetail detail = buildDetail(waybill, contract); + ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), settlementType(request.getSettlementType())); save(detail); - ReceivablePayableCargoFee cargoFee = buildCargoFee(detail.getId(), waybill); - cargoFeeMapper.insert(cargoFee); + calculatedFees(waybill, contract, request.getBillingPlanId()).forEach(fee -> { + fee.setDetailId(detail.getId()); + cargoFeeMapper.insert(fee); + }); } } + @Override + @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, + rollbackFor = Exception.class) + public void generateForCompletedWaybills(List waybillIds) { + if (Func.isEmpty(waybillIds)) return; + for (Long waybillId : waybillIds) { + Waybill waybill = waybillService.getById(waybillId); + if (waybill == null || Func.isEmpty(waybill.getContractId())) continue; + ContractManage contract = contractManageService.getById(waybill.getContractId()); + if (contract == null || !isSystemGeneration(contract)) continue; + try { + String planId = matchedPlanId(waybill, contract); + if (Func.isEmpty(planId)) continue; + List matchedFees = calculatedFees(waybill, contract, planId, true); + if (matchedFees.isEmpty()) continue; + for (String settlementType : List.of("payable", "receivable")) { + if (existsByWaybill(waybill.getId(), settlementType)) continue; + ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees); + save(detail); + for (ReceivablePayableCargoFee fee : matchedFees) { + ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); + copy.setId(null); + copy.setDetailId(detail.getId()); + cargoFeeMapper.insert(copy); + } + } + } catch (Exception exception) { + log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, contractId:{}, failureReason:{}", + waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception); + if (exception instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new RuntimeException(exception); + } + } + } + + private boolean isSystemGeneration(ContractManage contract) { + if (Func.isNotEmpty(contract.getFeeGenerationMode())) { + return "system".equalsIgnoreCase(contract.getFeeGenerationMode()) || "系统生成".equals(contract.getFeeGenerationMode()); + } + return !Integer.valueOf(0).equals(contract.getBillingEnabled()); + } + + private String matchedPlanId(Waybill waybill, ContractManage contract) { + List> plans = parseList(contract.getBillingPlanJson()); + if (plans.isEmpty()) return null; + Map plan = plans.stream().filter(item -> Boolean.TRUE.equals(item.get("defaultPlan"))).findFirst() + .orElse(plans.get(plans.size() - 1)); + if (!(plan.get("rules") instanceof List rules)) return null; + boolean matched = rules.stream().anyMatch(value -> value instanceof Map raw && matchesRule(raw, waybill)); + if (!matched) return null; + return "__matched__"; + } + + private boolean matchesRule(Map raw, Waybill waybill) { + Object conditionValue = raw.get("matchCondition"); + if (!(conditionValue instanceof Map condition)) return true; + return matchesLocation(condition, "origin", waybill.getDepartureAddressId(), waybill.getDepartureName(), waybill.getDepartureAddress()) + && matchesLocation(condition, "destination", waybill.getArrivalAddressId(), waybill.getArrivalName(), waybill.getArrivalAddress()) + && matchesCondition(condition.get("cargoType"), waybill.getCargoType()); + } + + private boolean matchesLocation(Map condition, String location, Long addressId, String addressName, + String detailAddress) { + Object expectedName = condition.get(location); + Object expectedCode = condition.get(location + "Code"); + if (isBlank(expectedName) && isBlank(expectedCode)) return true; + String actualCode = resolveRegionCode(addressId); + if (!isBlank(expectedCode) && !isBlank(actualCode)) { + return matchesCondition(expectedCode, actualCode); + } + return matchesCondition(expectedName, addressName) + || matchesCondition(expectedName, detailAddress); + } + + private String resolveRegionCode(Long addressId) { + if (addressId == null) return ""; + try { + CommonAddress address = commonAddressService.getById(addressId); + return address == null || address.getRegionCode() == null ? "" : address.getRegionCode().trim(); + } catch (Exception exception) { + log.warn("解析运单行政区编码失败,addressId:{}", addressId, exception); + return ""; + } + } + + private boolean isBlank(Object value) { + return value == null || String.valueOf(value).trim().isEmpty(); + } + + private boolean matchesCondition(Object expected, String actual) { + if (expected == null || String.valueOf(expected).isBlank()) return true; + if (expected instanceof List values) { + return values.stream().anyMatch(value -> matchesCondition(value, actual)); + } + String expectedText = String.valueOf(expected).trim(); + String actualText = String.valueOf(actual == null ? "" : actual).trim(); + if (expectedText.isEmpty() || actualText.isEmpty()) return false; + if (expectedText.equals(actualText) || actualText.contains(expectedText) || expectedText.contains(actualText)) { + return true; + } + return false; + } + private LambdaQueryWrapper buildQuery(ReceivablePayableDetailVO query) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(ReceivablePayableDetail::getIsDeleted, 0) @@ -251,6 +467,7 @@ public class ReceivablePayableDetailServiceImpl .like(Func.isNotEmpty(query.getBatchNo()), ReceivablePayableDetail::getBatchNo, query.getBatchNo()) .like(Func.isNotEmpty(query.getVehicleNo()), ReceivablePayableDetail::getVehicleNo, query.getVehicleNo()) .eq(Func.isNotEmpty(query.getSettlementStatus()), ReceivablePayableDetail::getSettlementStatus, query.getSettlementStatus()); + wrapper.eq(Func.isNotEmpty(query.getSettlementType()), ReceivablePayableDetail::getSettlementType, query.getSettlementType()); return wrapper.orderByDesc(ReceivablePayableDetail::getCreateTime); } @@ -258,12 +475,10 @@ public class ReceivablePayableDetailServiceImpl LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(ReceivablePayableDetail::getIsDeleted, 0) .eq(ReceivablePayableDetail::getSettlementStatus, "pending"); - if (Func.isNotEmpty(request.getIds())) { - wrapper.in(ReceivablePayableDetail::getId, request.getIds()); - } - if (Func.isNotEmpty(request.getContractId())) { - wrapper.eq(ReceivablePayableDetail::getContractId, request.getContractId()); + if (Func.isNotEmpty(request.getSettlementType())) { + wrapper.eq(ReceivablePayableDetail::getSettlementType, settlementType(request.getSettlementType())); } + wrapper.eq(ReceivablePayableDetail::getContractId, request.getContractId()); return wrapper; } @@ -272,7 +487,8 @@ public class ReceivablePayableDetailServiceImpl .eq(Waybill::getIsDeleted, 0) .eq(Waybill::getContractId, request.getContractId()) .eq(Waybill::getBusinessStatus, "completed") - .notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail where is_deleted = 0"); + .notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail where is_deleted = 0" + + (Func.isNotEmpty(request.getSettlementType()) ? " and settlement_type = '" + settlementType(request.getSettlementType()) + "'" : "")); if (Func.isNotEmpty(request.getBatchNo())) { wrapper.like(Waybill::getBatchNo, request.getBatchNo()); } @@ -288,20 +504,29 @@ public class ReceivablePayableDetailServiceImpl return wrapper.orderByDesc(Waybill::getCreateTime); } - private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract) { - ReceivablePayableCargoFee cargoFee = buildCargoFee(null, waybill); + private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String billingPlanId, String settlementType) { + List fees = calculatedFees(waybill, contract, billingPlanId); + return buildDetail(waybill, contract, settlementType, fees); + } + + private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String settlementType, List fees) { + BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP); ReceivablePayableDetail detail = new ReceivablePayableDetail(); detail.setDocumentNo(nextDocumentNo()); - detail.setSettlementType("receivable"); + detail.setSettlementType(settlementType); detail.setProjectId(waybill.getProjectId()); detail.setProjectName(waybill.getProjectName()); detail.setDeptId(waybill.getDeptId()); detail.setDeptName(waybill.getDeptName()); detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate()); - detail.setCustomerName(waybill.getCustomerName()); - detail.setContractId(waybill.getContractId()); + detail.setCustomerName("payable".equals(settlementType) + ? (contract == null ? waybill.getCarrierName() : contract.getPartyA()) + : (contract == null ? waybill.getCustomerName() : contract.getPartyB())); + detail.setContractId(contract == null ? waybill.getContractId() : contract.getId()); detail.setContractNo(contract == null ? null : contract.getContractNo()); - detail.setContractName(waybill.getContractName()); + detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName()); detail.setSourceType("系统生成"); detail.setWaybillId(waybill.getId()); detail.setWaybillNo(waybill.getWaybillNo()); @@ -315,11 +540,12 @@ public class ReceivablePayableDetailServiceImpl detail.setBatchNo(waybill.getBatchNo()); detail.setUnitPrice(waybill.getUnitPrice()); detail.setCurrency("RMB"); - detail.setFreightAmount(cargoFee.getFreightAmount()); - detail.setOtherFeeAmount(waybill.getOtherFeeTotal()); - detail.setTotalAmount(cargoFee.getAfterAmount()); + detail.setFreightAmount(freight); + detail.setOtherFeeAmount(other); + detail.setTotalAmount(total); detail.setSettlementStatus("pending"); - detail.setFeeItemsJson(cargoFee.getFeeItemsJson()); + detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, + (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); return detail; } @@ -358,6 +584,133 @@ public class ReceivablePayableDetailServiceImpl return cargoFee; } + private List calculatedFees(Waybill waybill, ContractManage contract, String planId) { + return calculatedFees(waybill, contract, planId, false); + } + + private List calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) { + List> plans = parseList(contract == null ? null : contract.getBillingPlanJson()); + Map plan = "__matched__".equals(planId) ? plans.stream().filter(this::isDefaultPlan).findFirst().orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1)) : plans.stream().filter(item -> Objects.equals(stringValue(item, "id"), planId) + || Objects.equals(stringValue(item, "planId"), planId)).findFirst() + .orElseGet(() -> plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null)); + if (plan == null || !(plan.get("rules") instanceof List)) return matchOnly ? List.of() : List.of(buildCargoFee(null, waybill)); + List result = new ArrayList<>(); + int line = 1; + for (Object value : (List) plan.get("rules")) { + if (!(value instanceof Map raw)) continue; + Map rule = new LinkedHashMap<>(); + raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); + if (matchOnly && !matchesRule(raw, waybill)) continue; + BigDecimal amount = calculateRule(rule, waybill); + if (amount == null) continue; + ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); + fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); + fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType()); + fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); + fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(stringValue(rule, "billingUnit", waybill.getQuantityUnit())); + fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); + fee.setMileage(waybill.getMileage()); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); + fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount))); + fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); + result.add(fee); + } + return result.isEmpty() && !matchOnly ? List.of(buildCargoFee(null, waybill)) : result; + } + + private boolean isDefaultPlan(Map plan) { + Object value = plan.get("defaultPlan"); + return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(String.valueOf(value)); + } + + private BigDecimal calculateRule(Map rule, Waybill waybill) { + String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", ""); + BigDecimal base = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice")); + if ("按重量".equals(element) || "按吨·公里".equals(element)) { + BigDecimal minimum = decimal(rule.get("minimumBillingWeight")); + if (minimum.signum() > 0 && "按重量".equals(element)) base = base.max(minimum); + if (minimum.signum() > 0 && "按吨·公里".equals(element)) base = base.divide(money(waybill.getQuantity()).max(BigDecimal.ONE), 6, RoundingMode.HALF_UP).multiply(minimum).multiply(money(waybill.getMileage())); + } + if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP); + List> ranges = ranges(rule); + if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); + if (type.contains("区间") && type.contains("一口价")) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); + if ("区间单价".equals(type)) { BigDecimal rangeBase = base; return range(ranges, rangeBase).map(r -> decimal(r.get("unitPrice")).multiply(rangeBase)).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); } + if ("阶梯单价".equals(type)) { + BigDecimal total = BigDecimal.ZERO, previous = BigDecimal.ZERO; + for (Map r : ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList()) { + BigDecimal upper = decimal(r.get("upperLimit")); BigDecimal part = base.min(upper).subtract(previous).max(BigDecimal.ZERO); + total = total.add(part.multiply(decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice")))); previous = upper; + if (base.compareTo(upper) <= 0) break; + } + return total.setScale(2, RoundingMode.HALF_UP); + } + return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); + } + + private boolean isFreight(ReceivablePayableCargoFee fee) { + return fee.getFreightAmount() != null && fee.getFreightAmount().compareTo(BigDecimal.ZERO) > 0; + } + + private boolean isFreightRule(Map rule) { + String type = stringValue(rule, "feeType", "") + stringValue(rule, "feeItem", ""); + return type.contains("运费") || type.contains("运输费"); + } + + private BigDecimal measure(Map rule, Waybill waybill) { + return switch (stringValue(rule, "billingElement", "按重量")) { + case "按体积" -> volume(waybill); + case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE; + case "按里程" -> money(waybill.getMileage()); + case "按吨·公里" -> money(waybill.getQuantity()).multiply(money(waybill.getMileage())); + case "按数量" -> money(waybill.getQuantity()); + default -> money(waybill.getQuantity()); + }; + } + + private BigDecimal volume(Waybill waybill) { + Map goods = parseMap(waybill.getGoodsJson()); + BigDecimal value = decimal(goods.get("volume")); + if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); + return value; + } + + private List> ranges(Map rule) { + Object source = rule.get("limitRanges"); + if (!(source instanceof List)) { Map fallback = new LinkedHashMap<>(); fallback.put("lowerLimit", rule.get("lowerLimit")); fallback.put("upperLimit", rule.get("upperLimit")); fallback.put("unitPrice", rule.get("unitPrice")); source = List.of(fallback); } + List> result = new ArrayList<>(); + for (Object value : (List) source) if (value instanceof Map raw) { + Map item = new LinkedHashMap<>(); raw.forEach((key, val) -> item.put(String.valueOf(key), val)); + if (item.get("lowerLimit") != null && item.get("upperLimit") != null) result.add(item); + } + return result; + } + + private Optional> range(List> ranges, BigDecimal value) { + List> sorted = ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList(); + for (int i = 0; i < sorted.size(); i++) { + Map r = sorted.get(i); + boolean upperMatched = i == sorted.size() - 1 ? value.compareTo(decimal(r.get("upperLimit"))) <= 0 : value.compareTo(decimal(r.get("upperLimit"))) < 0; + if (value.compareTo(decimal(r.get("lowerLimit"))) >= 0 && upperMatched) return Optional.of(r); + } + return Optional.empty(); + } + + private List> parseList(String json) { + if (Func.isEmpty(json)) return List.of(); + try { + Object parsed = JsonUtil.parse(json, List.class); + if (parsed instanceof List list) return list.stream().filter(Map.class::isInstance).map(item -> { + Map result = new LinkedHashMap<>(); ((Map) item).forEach((k, v) -> result.put(String.valueOf(k), v)); return result; + }).toList(); + } catch (Exception ignored) { } + return List.of(); + } + + private String stringValue(Map map, String key) { return stringValue(map, key, ""); } + private String stringValue(Map map, String key, String fallback) { + Object value = map.get(key); return value == null || String.valueOf(value).isBlank() ? fallback : String.valueOf(value); + } + private ReceivablePayableFeeDetailVO buildFeeDetail(List rows) { Set feeItemNames = new LinkedHashSet<>(); List records = rows.stream().map(row -> { @@ -379,26 +732,93 @@ public class ReceivablePayableDetailServiceImpl return vo; } - private void rebuildDetailFee(ReceivablePayableDetail detail) { + private List contractFeeItemNames(Long contractId) { + if (contractId == null) return List.of(); + ContractManage contract = contractManageService.getById(contractId); + if (contract == null) return List.of(); + LinkedHashSet names = new LinkedHashSet<>(); + for (Map plan : parseList(contract.getBillingPlanJson())) { + if (!(plan.get("rules") instanceof List rules)) continue; + for (Object value : rules) { + if (!(value instanceof Map raw)) continue; + Object feeItem = raw.get("feeItem"); + if (!isBlank(feeItem)) names.add(String.valueOf(feeItem)); + } + } + return new ArrayList<>(names); + } + + private void validateNonNegative(BigDecimal value, String field) { + if (value != null && value.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException(field + "不能小于0"); + } + } + + private BigDecimal adjustedAfterAmount(BigDecimal freightAmount, Map feeItems) { + BigDecimal feeItemTotal = feeItems.values().stream().map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + return (containsFreight ? feeItemTotal : freightAmount.add(feeItemTotal)).setScale(2, RoundingMode.HALF_UP); + } + + private boolean isFreightFeeItem(String name) { + return name != null && (name.contains("运费") || name.contains("运输费")); + } + + private void appendChange(List changes, String field, BigDecimal before, BigDecimal after) { + BigDecimal oldValue = money(before); + BigDecimal newValue = money(after); + if (oldValue.compareTo(newValue) != 0) { + changes.add("【" + field + "】从[" + formatValue(oldValue) + "]调整为[" + formatValue(newValue) + "]"); + } + } + + private String formatValue(BigDecimal value) { + return money(value).stripTrailingZeros().toPlainString(); + } + + private void refreshAdjustedDetail(ReceivablePayableDetail detail, List rows) { + BigDecimal freight = rows.stream().map(row -> money(row.getFreightAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal total = rows.stream().map(row -> money(row.getAfterAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); + Map feeItems = new LinkedHashMap<>(); + rows.forEach(row -> parseMap(row.getFeeItemsJson()).forEach((name, value) -> + feeItems.merge(name, decimal(value), BigDecimal::add))); + if (!rows.isEmpty()) { + detail.setTransportQuantity(rows.get(0).getTransportQuantity()); + detail.setMileage(rows.get(0).getMileage()); + } + detail.setFreightAmount(freight); + detail.setOtherFeeAmount(total.subtract(freight)); + detail.setTotalAmount(total); + detail.setFeeItemsJson(JsonUtil.toJson(feeItems)); + updateById(detail); + } + + private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) { Waybill waybill = waybillService.getById(detail.getWaybillId()); if (waybill == null) { throw new ServiceException("关联运单不存在"); } - ReceivablePayableCargoFee cargoFee = buildCargoFee(detail.getId(), waybill); + ContractManage contract = contractManageService.getById(detail.getContractId()); + List fees = calculatedFees(waybill, contract, billingPlanId); cargoFeeMapper.delete(Wrappers.lambdaQuery().eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); - cargoFeeMapper.insert(cargoFee); - detail.setFreightAmount(cargoFee.getFreightAmount()); - detail.setOtherFeeAmount(money(waybill.getOtherFeeTotal())); - detail.setTotalAmount(cargoFee.getAfterAmount()); - detail.setFeeItemsJson(cargoFee.getFeeItemsJson()); + fees.forEach(fee -> { fee.setDetailId(detail.getId()); cargoFeeMapper.insert(fee); }); + BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + detail.setFreightAmount(freight); + detail.setOtherFeeAmount(total.subtract(freight)); + detail.setTotalAmount(total); + detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); updateById(detail); } - private void closeDetails(List ids) { + private void closeDetails(List ids, String settlementType) { if (Func.isEmpty(ids)) { throw new ServiceException("请选择需要关闭的明细"); } for (ReceivablePayableDetail detail : listByIds(ids)) { + if (Func.isNotEmpty(settlementType) && !Objects.equals(detail.getSettlementType(), settlementType(settlementType))) { + throw new ServiceException("费用明细结算类型不匹配"); + } if (!"pending".equals(detail.getSettlementStatus())) { throw new ServiceException("仅待结算明细允许关闭"); } @@ -408,9 +828,13 @@ public class ReceivablePayableDetailServiceImpl } private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason) { + saveChangeRecord(detail, content, reason, "0001"); + } + + private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason, String lineNo) { ReceivablePayableChangeRecord record = new ReceivablePayableChangeRecord(); record.setDetailId(detail.getId()); - record.setLineNo("0001"); + record.setLineNo(lineNo); record.setCargoName(detail.getCargoName()); record.setChangeContent(content); record.setAdjustUser(AuthUtil.getUserId()); @@ -420,6 +844,16 @@ public class ReceivablePayableDetailServiceImpl changeRecordMapper.insert(record); } + private void applyManualAdjustment(ReceivablePayableDetail detail, BigDecimal amount, String feeItem, String reason) { + ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); + fee.setDetailId(detail.getId()); fee.setWaybillId(detail.getWaybillId()); fee.setLineNo("ADJ-" + System.currentTimeMillis()); + fee.setCargoName(Func.isEmpty(feeItem) ? "手工调差" : feeItem); fee.setBillingFactor("手工调整"); fee.setBillingType("手工调差"); + fee.setOriginalAmount(BigDecimal.ZERO); fee.setAdjustAmount(money(amount)); fee.setAfterAmount(money(amount)); fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), money(amount)))); fee.setRemark(reason); + cargoFeeMapper.insert(fee); + detail.setOtherFeeAmount(money(detail.getOtherFeeAmount()).add(money(amount))); + detail.setTotalAmount(money(detail.getTotalAmount()).add(money(amount))); updateById(detail); + } + private ReceivablePayableDetail getExisting(Long id) { ReceivablePayableDetail detail = getById(id); if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) { @@ -428,12 +862,21 @@ public class ReceivablePayableDetailServiceImpl return detail; } - private boolean existsByWaybill(Long waybillId) { + private boolean existsByWaybill(Long waybillId, String settlementType) { return count(Wrappers.lambdaQuery() .eq(ReceivablePayableDetail::getWaybillId, waybillId) + .eq(ReceivablePayableDetail::getSettlementType, settlementType) .eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0; } + private String settlementType(String value) { + if (Func.isEmpty(value)) return "receivable"; + if (!List.of("receivable", "payable").contains(value)) { + throw new ServiceException("结算类型不正确"); + } + return value; + } + private void validateGenerateRequest(ReceivablePayableGenerateRequest request, boolean requireWaybill) { if (Func.isEmpty(request.getContractId())) { throw new ServiceException("请选择运单合同"); @@ -441,6 +884,7 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(request.getBillingPlanId())) { throw new ServiceException("请选择计费方案"); } + settlementType(request.getSettlementType()); if (requireWaybill && Func.isEmpty(request.getWaybillIds())) { throw new ServiceException("请选择需要生成费用的运单"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 66fb490..60b3984 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -42,11 +42,13 @@ import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.service.ILoadingManageService; +import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.wrapper.WaybillWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.util.ArrayList; @@ -62,6 +64,7 @@ import java.util.stream.Collectors; * @author Chill */ @Service +@Slf4j public class WaybillServiceImpl extends BaseServiceImpl implements IWaybillService { private static final String STATUS_DRAFT = "draft"; @@ -69,6 +72,10 @@ public class WaybillServiceImpl extends BaseServiceImpl @jakarta.annotation.Resource private ILoadingManageService loadingManageService; + @jakarta.annotation.Resource + @org.springframework.context.annotation.Lazy + private IReceivablePayableDetailService receivablePayableDetailService; + @Override public IPage selectWaybillPage(IPage page, WaybillVO waybill) { IPage entityPage = page(page, buildQuery(waybill)); @@ -284,7 +291,11 @@ public class WaybillServiceImpl extends BaseServiceImpl throw new ServiceException("当前状态不允许完成"); } waybill.setBusinessStatus("completed"); - return updateById(waybill); + boolean updated = updateById(waybill); + if (updated) { + receivablePayableDetailService.generateForCompletedWaybills(List.of(waybill.getId())); + } + return updated; } @Override @@ -300,6 +311,8 @@ public class WaybillServiceImpl extends BaseServiceImpl complete(waybill.getId()); result.setSuccessCount(result.getSuccessCount() + 1); } catch (Exception exception) { + log.error("批量完成运单失败,waybillId:{}, waybillNo:{}, failureReason:{}", + waybill.getId(), waybill.getWaybillNo(), exception.getMessage(), exception); result.setSkippedCount(result.getSkippedCount() + 1); result.getSkippedCodes().add(waybill.getWaybillNo()); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InsuranceOcrTemplateWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InsuranceOcrTemplateWrapper.java new file mode 100644 index 0000000..b351993 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InsuranceOcrTemplateWrapper.java @@ -0,0 +1,55 @@ +/** + * 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.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.InsuranceOcrTemplate; +import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO; + +import java.util.Objects; + +/** + * 保险OCR识别模板包装类。 + * + * @author Chill + */ +public class InsuranceOcrTemplateWrapper extends BaseEntityWrapper { + + public static InsuranceOcrTemplateWrapper build() { + return new InsuranceOcrTemplateWrapper(); + } + + @Override + public InsuranceOcrTemplateVO entityVO(InsuranceOcrTemplate insuranceOcrTemplate) { + InsuranceOcrTemplateVO insuranceOcrTemplateVO = Objects.requireNonNull(BeanUtil.copyProperties(insuranceOcrTemplate, InsuranceOcrTemplateVO.class)); + insuranceOcrTemplateVO.setCreateUserName(UserCache.getUserRealName(insuranceOcrTemplate.getCreateUser())); + insuranceOcrTemplateVO.setUpdateUserName(UserCache.getUserRealName(insuranceOcrTemplate.getUpdateUser())); + return insuranceOcrTemplateVO; + } + +} diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index ed1dfe1..1fcdc23 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -94,6 +94,17 @@ thirdParty: # MK开放接口地址 baseUrl: http://127.0.0.1:8080 +#百度OCR配置,API Key和Secret Key请通过环境变量注入 +baidu: + ocr: + enabled: ${BAIDU_OCR_ENABLED:false} + api-key: ${BAIDU_OCR_API_KEY:} + secret-key: ${BAIDU_OCR_SECRET_KEY:} + endpoint: ${BAIDU_OCR_ENDPOINT:https://aip.baidubce.com} + connect-timeout: 5s + request-timeout: 30s + token-refresh-advance: 1m + powerjob: worker: diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 5fb7e2f..d8f6404 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -66,6 +66,17 @@ thirdParty: # MK开放接口地址 baseUrl: http://127.0.0.1:8080 +#百度OCR配置,API Key和Secret Key请通过环境变量注入 +baidu: + ocr: + enabled: ${BAIDU_OCR_ENABLED:false} + api-key: ${BAIDU_OCR_API_KEY:} + secret-key: ${BAIDU_OCR_SECRET_KEY:} + endpoint: ${BAIDU_OCR_ENDPOINT:https://aip.baidubce.com} + connect-timeout: 5s + request-timeout: 30s + token-refresh-advance: 1m + powerjob: worker: server-address: 172.16.203.228:7700 diff --git a/doc/nacos/blade-test.yaml b/doc/nacos/blade-test.yaml index 4223a91..2839eb5 100644 --- a/doc/nacos/blade-test.yaml +++ b/doc/nacos/blade-test.yaml @@ -43,3 +43,14 @@ blade: url: jdbc:mysql://192.168.0.188:3306/bladex?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true username: root password: root + +#百度OCR配置,API Key和Secret Key请通过环境变量注入 +baidu: + ocr: + enabled: ${BAIDU_OCR_ENABLED:false} + api-key: ${BAIDU_OCR_API_KEY:} + secret-key: ${BAIDU_OCR_SECRET_KEY:} + endpoint: ${BAIDU_OCR_ENDPOINT:https://aip.baidubce.com} + connect-timeout: 5s + request-timeout: 30s + token-refresh-advance: 1m diff --git a/doc/sql/transport/blade_contract_manage_fee_config_20260817.sql b/doc/sql/transport/blade_contract_manage_fee_config_20260817.sql new file mode 100644 index 0000000..fedd8ec --- /dev/null +++ b/doc/sql/transport/blade_contract_manage_fee_config_20260817.sql @@ -0,0 +1,6 @@ +-- 合同费用生成、结算配置及付款比例设置 +ALTER TABLE blade_contract_manage + ADD COLUMN fee_generation_mode varchar(20) DEFAULT 'system' COMMENT '费用生成模式:system系统生成,manual手动生成' AFTER billing_enabled, + ADD COLUMN pre_settlement_config_json text DEFAULT NULL COMMENT '预结算配置JSON' AFTER settlement_rule_json, + ADD COLUMN formal_settlement_config_json text DEFAULT NULL COMMENT '正式结算配置JSON' AFTER pre_settlement_config_json, + ADD COLUMN payment_ratio_json text DEFAULT NULL COMMENT '付款比例设置JSON' AFTER formal_settlement_config_json; diff --git a/doc/sql/transport/blade_insurance_ocr_template.sql b/doc/sql/transport/blade_insurance_ocr_template.sql new file mode 100644 index 0000000..8bd1f7f --- /dev/null +++ b/doc/sql/transport/blade_insurance_ocr_template.sql @@ -0,0 +1,32 @@ +-- ---------------------------- +-- Table structure for blade_insurance_ocr_template +-- ---------------------------- +DROP TABLE IF EXISTS `blade_insurance_ocr_template`; +CREATE TABLE `blade_insurance_ocr_template` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板名称', + `mapping_config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '字段映射配置JSON', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `status` int(11) NOT NULL DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否已删除', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_insurance_ocr_template_name` (`tenant_id`, `name`) USING BTREE, + KEY `idx_insurance_ocr_template_status` (`status`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='保险OCR识别模板'; + +-- ---------------------------- +-- Menu data for insurance OCR template +-- parent_id:基础配置 1164733399668962201 +-- ---------------------------- +INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES +(2086000000000000001, 1164733399668962201, 'insurance_ocr_template', '保险OCR识别模板', 'insurance_ocr_template', '/base/insurance-ocr-template', 'iconfont iconicon_doc', 90, 1, 0, 1, NULL, '', 0), +(2086000000000000002, 2086000000000000001, 'insurance_ocr_template_add', '新增', 'insurance_ocr_template_add', '', '', 1, 2, 0, 1, NULL, '', 0), +(2086000000000000003, 2086000000000000001, 'insurance_ocr_template_edit', '编辑', 'insurance_ocr_template_edit', '', '', 2, 2, 0, 1, NULL, '', 0), +(2086000000000000004, 2086000000000000001, 'insurance_ocr_template_delete', '删除', 'insurance_ocr_template_delete', '', '', 3, 2, 0, 1, NULL, '', 0), +(2086000000000000005, 2086000000000000001, 'insurance_ocr_template_view', '查看', 'insurance_ocr_template_view', '', '', 4, 2, 0, 1, NULL, '', 0), +(2086000000000000006, 2086000000000000001, 'insurance_ocr_template_list', '列表', 'insurance_ocr_template_list', '/blade-transport/insurance-ocr-template/list', '', 5, 2, 0, 1, NULL, '', 0); From 2aefc21822c76f92036010b011d3427bb93b2482 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 12:33:47 +0800 Subject: [PATCH 019/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dnacos=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-service/blade-system/src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 02ca580..36bd4ce 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -8,7 +8,6 @@ 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 b62247e83f9f2253110884e60c1d21d98afc8414 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 12:33:47 +0800 Subject: [PATCH 020/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dnacos=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-service/blade-system/src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 02ca580..36bd4ce 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -8,7 +8,6 @@ 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 cb425028c03501415538199dbe077171e44874cf Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 12:40:07 +0800 Subject: [PATCH 021/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dnacos=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/src/main/resources/application.yml | 1 - blade-service/blade-file/src/main/resources/application.yml | 1 - blade-service/blade-openapi/src/main/resources/application.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index feef54f..1cbfc4b 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -8,7 +8,6 @@ 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: diff --git a/blade-service/blade-file/src/main/resources/application.yml b/blade-service/blade-file/src/main/resources/application.yml index 20bd504..53557ba 100644 --- a/blade-service/blade-file/src/main/resources/application.yml +++ b/blade-service/blade-file/src/main/resources/application.yml @@ -8,7 +8,6 @@ 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: diff --git a/blade-service/blade-openapi/src/main/resources/application.yml b/blade-service/blade-openapi/src/main/resources/application.yml index 90dea1e..af16a12 100644 --- a/blade-service/blade-openapi/src/main/resources/application.yml +++ b/blade-service/blade-openapi/src/main/resources/application.yml @@ -8,7 +8,6 @@ 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 - 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: From 6c64b32a4a6e27986432ff2b193f817e30743ac2 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 13:00:23 +0800 Subject: [PATCH 022/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dnacos=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/constant/LauncherConstant.java | 37 ++++++++++++++++++- .../common/launch/LauncherServiceImpl.java | 8 ++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/blade-common/src/main/java/org/springblade/common/constant/LauncherConstant.java b/blade-common/src/main/java/org/springblade/common/constant/LauncherConstant.java index a7ff128..096cab3 100644 --- a/blade-common/src/main/java/org/springblade/common/constant/LauncherConstant.java +++ b/blade-common/src/main/java/org/springblade/common/constant/LauncherConstant.java @@ -54,6 +54,12 @@ public interface LauncherConstant { */ String NACOS_PROD_ADDR = "172.16.203.228:8848"; + String NACOS_PROD_HOST = "172.16.203.228:8848"; + + String NACOS_PROD_USERNAME = "nacos"; + + String NACOS_PROD_PASSWORD = "nacosTMS"; + /** * nacos test 地址 */ @@ -156,13 +162,42 @@ public interface LauncherConstant { * @return addr */ static String nacosAddr(String profile) { + String profileKey = profile == null ? "" : profile.toUpperCase(); + String profileAddr = env("NACOS_" + profileKey + "_HOST"); + if (profileAddr != null) return profileAddr; + String configuredAddr = env("NACOS_HOST"); + if (configuredAddr != null) return configuredAddr; return switch (profile) { - case (AppConstant.PROD_CODE) -> NACOS_PROD_ADDR; + case (AppConstant.PROD_CODE) -> NACOS_PROD_HOST; case (AppConstant.TEST_CODE) -> NACOS_TEST_ADDR; default -> NACOS_DEV_ADDR; }; } + static String nacosUsername(String profile) { + String profileKey = profile == null ? "" : profile.toUpperCase(); + String value = env("NACOS_" + profileKey + "_USERNAME"); + if (value != null) return value; + value = env("NACOS_USERNAME"); + if (value != null) return value; + return AppConstant.PROD_CODE.equals(profile) ? NACOS_PROD_USERNAME : NACOS_USERNAME; + } + + static String nacosPassword(String profile) { + String profileKey = profile == null ? "" : profile.toUpperCase(); + String value = env("NACOS_" + profileKey + "_PASSWORD"); + if (value != null) return value; + value = env("NACOS_PASSWORD"); + if (value != null) return value; + return AppConstant.PROD_CODE.equals(profile) ? NACOS_PROD_PASSWORD : NACOS_PASSWORD; + } + + static String env(String name) { + String value = System.getProperty(name); + if (value == null || value.isBlank()) value = System.getenv(name); + return value == null || value.isBlank() ? null : value.trim(); + } + /** * 动态获取sentinel地址 * 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 d50ff5e..f2a7568 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 @@ -48,12 +48,12 @@ public class LauncherServiceImpl implements LauncherService { 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.username", LauncherConstant.nacosUsername(profile)); + PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.password", LauncherConstant.nacosPassword(profile)); 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.username", LauncherConstant.nacosUsername(profile)); + PropsUtil.setProperty(props, "spring.cloud.nacos.config.password", LauncherConstant.nacosPassword(profile)); PropsUtil.setProperty(props, "spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile)); } From 5aceb6e8934a767d911bda541b1314938a0c9f5b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 14:57:42 +0800 Subject: [PATCH 023/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dnacos=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/launch/LauncherServiceImpl.java | 19 +++-- doc/nacos/blade-dev.yaml | 31 +++++++- doc/nacos/blade-prod.yaml | 31 +++++++- script/docker/app/deploy.sh | 2 +- script/docker/app/docker-compose.yml | 79 +++++++------------ 5 files changed, 99 insertions(+), 63 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 f2a7568..2d73b5c 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 @@ -47,14 +47,21 @@ public class LauncherServiceImpl implements LauncherService { Properties props = System.getProperties(); if (BladeApplication.isNacosConfigEnabled()) { + String nacosUsername = LauncherConstant.nacosUsername(profile); + String nacosPassword = LauncherConstant.nacosPassword(profile); + String nacosAddr = LauncherConstant.nacosAddr(profile); + // nacos公共配置,spring.config.import在配置中心和注册中心初始化前读取 + PropsUtil.setProperty(props, "spring.cloud.nacos.username", nacosUsername); + PropsUtil.setProperty(props, "spring.cloud.nacos.password", nacosPassword); + PropsUtil.setProperty(props, "spring.cloud.nacos.server-addr", nacosAddr); // nacos注册中心配置 - PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.username", LauncherConstant.nacosUsername(profile)); - PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.password", LauncherConstant.nacosPassword(profile)); - PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.server-addr", LauncherConstant.nacosAddr(profile)); + PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.username", nacosUsername); + PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.password", nacosPassword); + PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.server-addr", nacosAddr); // nacos配置中心配置 - PropsUtil.setProperty(props, "spring.cloud.nacos.config.username", LauncherConstant.nacosUsername(profile)); - PropsUtil.setProperty(props, "spring.cloud.nacos.config.password", LauncherConstant.nacosPassword(profile)); - PropsUtil.setProperty(props, "spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile)); + PropsUtil.setProperty(props, "spring.cloud.nacos.config.username", nacosUsername); + PropsUtil.setProperty(props, "spring.cloud.nacos.config.password", nacosPassword); + PropsUtil.setProperty(props, "spring.cloud.nacos.config.server-addr", nacosAddr); } // sentinel配置 diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index 1fcdc23..977a9db 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -91,8 +91,35 @@ thirdParty: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 mk: - # MK开放接口地址 - baseUrl: http://127.0.0.1:8080 + # MK应用及接口配置,敏感值通过环境变量注入 + appKey: ${MK_APP_KEY:} + appSecret: ${MK_APP_SECRET:} + baseUrl: ${MK_BASE_URL:http://127.0.0.1:8080} + loginUrl: ${MK_LOGIN_URL:http://127.0.0.1:8080} + oauthAppId: ${MK_OAUTH_APP_ID:} + oauthAppSecret: ${MK_OAUTH_APP_SECRET:} + subjectPrefix: ${MK_SUBJECT_PREFIX:} + templateCodePrefix: ${MK_TEMPLATE_CODE_PREFIX:} + mkSsoLoginUrl: ${MK_SSO_LOGIN_URL:/data/sys-oauth/ssoLogin} + checkReferer: ${MK_CHECK_REFERER:true} + erpBaseUrls: ${MK_ERP_BASE_URLS:http://127.0.0.1:2888} + getTokenUrl: ${MK_GET_TOKEN_URL:/authapi/getToken} + processSubmitUrl: ${MK_PROCESS_SUBMIT_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/submit} + processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute} + processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete} + getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo} + getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos} + getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode} + pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept} + pushPersonUrl: ${MK_PUSH_PERSON_URL:/openapi/sys-org/v2/push/person} + getOAuthCodeUrl: ${MK_OAUTH_CODE_URL:/openapi/sys-oauth/openOauth/getCode} + getOAuthTokenUrl: ${MK_OAUTH_TOKEN_URL:/openapi/sys-oauth/openOauth/getToken} + getOAuthUserInfoUrl: ${MK_OAUTH_USER_INFO_URL:/openapi/sys-oauth/openOauth/getUserInfo} + queryAuditNotesUrl: ${MK_QUERY_AUDIT_NOTES_URL:/openapi/sys-lbpm/lbpmAuditNote/openSupport/listNote} + querySenderListUrl: ${MK_QUERY_SENDER_LIST_URL:/openapi/sys-lbpm/sysLbpmProcessCard/openSupport/getSenderList} + downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download} + queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list} + queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list} #百度OCR配置,API Key和Secret Key请通过环境变量注入 baidu: diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index d8f6404..16c1b02 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -63,8 +63,35 @@ thirdParty: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 mk: - # MK开放接口地址 - baseUrl: http://127.0.0.1:8080 + # MK应用及接口配置,敏感值通过环境变量注入 + appKey: ${MK_APP_KEY:} + appSecret: ${MK_APP_SECRET:} + baseUrl: ${MK_BASE_URL:http://127.0.0.1:8080} + loginUrl: ${MK_LOGIN_URL:http://127.0.0.1:8080} + oauthAppId: ${MK_OAUTH_APP_ID:} + oauthAppSecret: ${MK_OAUTH_APP_SECRET:} + subjectPrefix: ${MK_SUBJECT_PREFIX:} + templateCodePrefix: ${MK_TEMPLATE_CODE_PREFIX:} + mkSsoLoginUrl: ${MK_SSO_LOGIN_URL:/data/sys-oauth/ssoLogin} + checkReferer: ${MK_CHECK_REFERER:true} + erpBaseUrls: ${MK_ERP_BASE_URLS:} + getTokenUrl: ${MK_GET_TOKEN_URL:/authapi/getToken} + processSubmitUrl: ${MK_PROCESS_SUBMIT_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/submit} + processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute} + processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete} + getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo} + getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos} + getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode} + pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept} + pushPersonUrl: ${MK_PUSH_PERSON_URL:/openapi/sys-org/v2/push/person} + getOAuthCodeUrl: ${MK_OAUTH_CODE_URL:/openapi/sys-oauth/openOauth/getCode} + getOAuthTokenUrl: ${MK_OAUTH_TOKEN_URL:/openapi/sys-oauth/openOauth/getToken} + getOAuthUserInfoUrl: ${MK_OAUTH_USER_INFO_URL:/openapi/sys-oauth/openOauth/getUserInfo} + queryAuditNotesUrl: ${MK_QUERY_AUDIT_NOTES_URL:/openapi/sys-lbpm/lbpmAuditNote/openSupport/listNote} + querySenderListUrl: ${MK_QUERY_SENDER_LIST_URL:/openapi/sys-lbpm/sysLbpmProcessCard/openSupport/getSenderList} + downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download} + queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list} + queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list} #百度OCR配置,API Key和Secret Key请通过环境变量注入 baidu: diff --git a/script/docker/app/deploy.sh b/script/docker/app/deploy.sh index c671b57..0c8bf3b 100644 --- a/script/docker/app/deploy.sh +++ b/script/docker/app/deploy.sh @@ -90,7 +90,7 @@ mount(){ #启动基础模块 base(){ - docker-compose up -d nacos sentinel seata-server web-nginx blade-nginx blade-redis powerjob-server + docker-compose up -d sentinel seata-server web-nginx blade-nginx blade-redis powerjob-server } #启动监控模块 diff --git a/script/docker/app/docker-compose.yml b/script/docker/app/docker-compose.yml index a5e6c79..2f1cdb0 100644 --- a/script/docker/app/docker-compose.yml +++ b/script/docker/app/docker-compose.yml @@ -1,32 +1,22 @@ version: '3' + +x-blade-environment: &blade-environment + - TZ=Asia/Shanghai + - SPRING_PROFILES_ACTIVE=prod + - NACOS_PROD_HOST=${NACOS_PROD_HOST:?NACOS_PROD_HOST must be set} + - NACOS_PROD_USERNAME=${NACOS_PROD_USERNAME:-admin} + - NACOS_PROD_PASSWORD=${NACOS_PROD_PASSWORD:-nacosTMS} + - MK_OAUTH_APP_ID=${MK_OAUTH_APP_ID:-} + - MK_OAUTH_APP_SECRET=${MK_OAUTH_APP_SECRET:-} + - MK_SUBJECT_PREFIX=${MK_SUBJECT_PREFIX:-} + - MK_TEMPLATE_CODE_PREFIX=${MK_TEMPLATE_CODE_PREFIX:-} + services: #################################################################################################### ###=================================== 以下为中间件模块 =========================================### #################################################################################################### - nacos: - image: nacos/nacos-server:v3.1.2 - hostname: "nacos-standalone" - environment: - - NACOS_AUTH_ENABLE=true - - NACOS_AUTH_CACHE_ENABLE=true - - NACOS_AUTH_IDENTITY_KEY=nacos - - NACOS_AUTH_IDENTITY_VALUE=nacos - - NACOS_AUTH_TOKEN= # 请阅读官方文档了解规则后替换为自己的token:https://nacos.io/zh-cn/docs/v2/guide/user/auth.html - - MODE=standalone - - TZ=Asia/Shanghai - volumes: - - /docker/nacos/standalone-logs/:/home/nacos/logs - - /docker/nacos/conf/application.properties:/home/nacos/conf/application.properties - ports: - - 8848:8848 - - 9848:9848 - - 8080:8080 - networks: - blade_net: - ipv4_address: 172.30.0.48 - sentinel: image: bladex/sentinel-dashboard:1.8.6 hostname: "sentinel" @@ -118,8 +108,7 @@ services: blade-admin: image: "${REGISTER}/blade-admin:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment ports: - 7002:7002 privileged: true @@ -130,8 +119,7 @@ services: blade-gateway1: image: "${REGISTER}/blade-gateway:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -140,8 +128,7 @@ services: blade-gateway2: image: "${REGISTER}/blade-gateway:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -150,8 +137,7 @@ services: blade-auth1: image: "${REGISTER}/blade-auth:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -160,8 +146,7 @@ services: blade-auth2: image: "${REGISTER}/blade-auth:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -170,8 +155,7 @@ services: blade-report: image: "${REGISTER}/blade-report:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always ports: @@ -182,8 +166,7 @@ services: blade-log: image: "${REGISTER}/blade-log:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -191,8 +174,7 @@ services: blade-desk: image: "${REGISTER}/blade-desk:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -200,8 +182,7 @@ services: blade-system: image: "${REGISTER}/blade-system:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -209,8 +190,7 @@ services: blade-flow: image: "${REGISTER}/blade-flow:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -218,8 +198,7 @@ services: blade-resource: image: "${REGISTER}/blade-resource:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -227,8 +206,7 @@ services: blade-job: image: "${REGISTER}/blade-job:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -236,8 +214,7 @@ services: blade-transport: image: "${REGISTER}/blade-transport:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always networks: @@ -245,8 +222,7 @@ services: blade-file: image: "${REGISTER}/blade-file:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always ports: @@ -256,8 +232,7 @@ services: blade-openapi: image: "${REGISTER}/blade-openapi:${TAG}" - environment: - - TZ=Asia/Shanghai + environment: *blade-environment privileged: true restart: always ports: From c8b8b59c508a371aa4cfc68a52ecfaff8bcdeac3 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 15:18:15 +0800 Subject: [PATCH 024/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dnacos=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/src/main/resources/application.yml | 6 +++--- blade-service/blade-file/src/main/resources/application.yml | 6 +++--- .../blade-openapi/src/main/resources/application.yml | 6 +++--- .../blade-system/src/main/resources/application.yml | 6 +++--- script/docker/app/docker-compose.yml | 3 +++ 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index 1cbfc4b..ae6f2c9 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -11,9 +11,9 @@ spring: - 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} + username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}} + password: ${NACOS_PASSWORD:${NACOS_PROD_PASSWORD:nacos}} + server-addr: ${NACOS_HOST:${NACOS_PROD_HOST:127.0.0.1:8848}} discovery: namespace: "${NACOS_NAMESPACE:}" config: diff --git a/blade-service/blade-file/src/main/resources/application.yml b/blade-service/blade-file/src/main/resources/application.yml index 53557ba..c182e2a 100644 --- a/blade-service/blade-file/src/main/resources/application.yml +++ b/blade-service/blade-file/src/main/resources/application.yml @@ -11,9 +11,9 @@ spring: - 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} + username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}} + password: ${NACOS_PASSWORD:${NACOS_PROD_PASSWORD:nacos}} + server-addr: ${NACOS_HOST:${NACOS_PROD_HOST:127.0.0.1:8848}} discovery: namespace: "${NACOS_NAMESPACE:}" config: diff --git a/blade-service/blade-openapi/src/main/resources/application.yml b/blade-service/blade-openapi/src/main/resources/application.yml index af16a12..be5d93f 100644 --- a/blade-service/blade-openapi/src/main/resources/application.yml +++ b/blade-service/blade-openapi/src/main/resources/application.yml @@ -12,9 +12,9 @@ spring: - 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} + username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}} + password: ${NACOS_PASSWORD:${NACOS_PROD_PASSWORD:nacos}} + server-addr: ${NACOS_HOST:${NACOS_PROD_HOST:127.0.0.1:8848}} discovery: namespace: "${NACOS_NAMESPACE:}" config: diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 36bd4ce..28509cf 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -11,9 +11,9 @@ spring: - 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} + username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}} + password: ${NACOS_PASSWORD:${NACOS_PROD_PASSWORD:nacos}} + server-addr: ${NACOS_HOST:${NACOS_PROD_HOST:127.0.0.1:8848}} discovery: namespace: "${NACOS_NAMESPACE:}" config: diff --git a/script/docker/app/docker-compose.yml b/script/docker/app/docker-compose.yml index 2f1cdb0..aab823c 100644 --- a/script/docker/app/docker-compose.yml +++ b/script/docker/app/docker-compose.yml @@ -3,6 +3,9 @@ version: '3' x-blade-environment: &blade-environment - TZ=Asia/Shanghai - SPRING_PROFILES_ACTIVE=prod + - NACOS_HOST=${NACOS_PROD_HOST:?NACOS_PROD_HOST must be set} + - NACOS_USERNAME=${NACOS_PROD_USERNAME:-admin} + - NACOS_PASSWORD=${NACOS_PROD_PASSWORD:-nacosTMS} - NACOS_PROD_HOST=${NACOS_PROD_HOST:?NACOS_PROD_HOST must be set} - NACOS_PROD_USERNAME=${NACOS_PROD_USERNAME:-admin} - NACOS_PROD_PASSWORD=${NACOS_PROD_PASSWORD:-nacosTMS} From 154cc4a7cb278e2e87964ea7b0c2d02ef8a71eec Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 21:31:59 +0800 Subject: [PATCH 025/114] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E9=A2=84?= =?UTF-8?q?=E7=BB=93=E7=AE=97=E5=8D=95=202=E3=80=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E7=BB=93=E7=AE=97=E5=8D=95=203=E3=80=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4IAM=E8=AE=A4=E8=AF=81=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/config/BladeAuthConfiguration.java | 15 + .../auth/granter/IamSsoTokenGranter.java | 43 +- blade-auth/src/main/resources/application.yml | 13 + .../gateway/provider/AuthProvider.java | 1 + .../springblade/system/feign/ISysClient.java | 9 + .../system/feign/ISysClientFallback.java | 5 + .../dto/FormalSettlementPaymentRequest.java | 23 + .../pojo/dto/FormalSettlementSaveRequest.java | 31 + .../dto/FormalSettlementStatusRequest.java | 21 + .../pojo/dto/PreSettlementAdvanceRequest.java | 40 + .../dto/PreSettlementDetailAdjustRequest.java | 77 + .../pojo/dto/PreSettlementSaveRequest.java | 99 ++ .../pojo/dto/PreSettlementStatusRequest.java | 42 + .../pojo/entity/FormalSettlement.java | 65 + .../pojo/entity/FormalSettlementDetail.java | 59 + .../entity/FormalSettlementDetailFee.java | 45 + .../pojo/entity/FormalSettlementPayment.java | 37 + .../pojo/entity/FormalSettlementSource.java | 35 + .../transport/pojo/entity/PreSettlement.java | 143 ++ .../pojo/entity/PreSettlementAdvance.java | 58 + .../entity/PreSettlementChangeRecord.java | 59 + .../pojo/entity/PreSettlementDetail.java | 117 ++ .../pojo/entity/PreSettlementDetailFee.java | 83 + .../pojo/entity/PreSettlementSummaryFee.java | 62 + .../transport/pojo/vo/FormalSettlementVO.java | 43 + .../transport/pojo/vo/PreSettlementVO.java | 108 ++ .../springblade/system/feign/IUserClient.java | 10 + .../springblade/system/feign/SysClient.java | 12 + .../springblade/system/feign/UserClient.java | 6 + .../system/service/IUserService.java | 8 + .../system/service/impl/UserServiceImpl.java | 30 + .../FormalSettlementController.java | 146 ++ .../controller/PreSettlementController.java | 262 +++ .../transport/excel/PreSettlementExcel.java | 72 + .../FormalSettlementDetailFeeMapper.java | 18 + .../mapper/FormalSettlementDetailMapper.java | 18 + .../mapper/FormalSettlementMapper.java | 18 + .../mapper/FormalSettlementPaymentMapper.java | 18 + .../mapper/FormalSettlementSourceMapper.java | 18 + .../mapper/PreSettlementAdvanceMapper.java | 22 + .../PreSettlementChangeRecordMapper.java | 22 + .../mapper/PreSettlementDetailFeeMapper.java | 22 + .../mapper/PreSettlementDetailMapper.java | 22 + .../transport/mapper/PreSettlementMapper.java | 22 + .../mapper/PreSettlementSummaryFeeMapper.java | 22 + .../service/IFormalSettlementService.java | 43 + .../service/IPreSettlementService.java | 71 + .../impl/FormalSettlementServiceImpl.java | 511 ++++++ .../impl/PreSettlementServiceImpl.java | 1404 +++++++++++++++++ .../wrapper/FormalSettlementWrapper.java | 45 + .../wrapper/PreSettlementWrapper.java | 51 + .../blade_formal_settlement_20260818.sql | 90 ++ .../blade_pre_settlement_20260818.sql | 214 +++ 53 files changed, 4517 insertions(+), 13 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java create mode 100644 doc/sql/transport/blade_formal_settlement_20260818.sql create mode 100644 doc/sql/transport/blade_pre_settlement_20260818.sql diff --git a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java index 15a2061..f587e7e 100644 --- a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java +++ b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java @@ -25,6 +25,7 @@ */ package org.springblade.auth.config; +import org.springblade.auth.granter.IamAwareTokenGranterFactory; import org.springblade.auth.handler.BladeAuthorizationHandler; import org.springblade.auth.handler.BladeLockHandler; import org.springblade.auth.handler.BladeLogHandler; @@ -37,6 +38,9 @@ import org.springblade.core.jwt.props.JwtProperties; import org.springblade.core.launch.props.BladeProperties; import org.springblade.core.launch.server.ServerInfo; import org.springblade.core.oauth2.config.OAuth2AutoConfiguration; +import org.springblade.core.oauth2.granter.TokenGranter; +import org.springblade.core.oauth2.granter.TokenGranterEnhancer; +import org.springblade.core.oauth2.granter.TokenGranterFactory; import org.springblade.core.oauth2.handler.AuthorizationHandler; import org.springblade.core.oauth2.handler.PasswordHandler; import org.springblade.core.oauth2.handler.TokenHandler; @@ -51,8 +55,11 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; +import java.util.List; + /** * BladeAuthConfiguration * @@ -101,4 +108,12 @@ public class BladeAuthConfiguration { return new BladeUserDetailService(userClient); } + @Primary + @Bean("iamAwareTokenGranterFactory") + public TokenGranterFactory tokenGranterFactory(List tokenGranters, + List tokenGranterEnhancers, + OAuth2Properties properties) { + return new IamAwareTokenGranterFactory(tokenGranters, tokenGranterEnhancers, properties); + } + } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index 523fe49..6c6d279 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -65,8 +65,10 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; +import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; /** @@ -167,7 +169,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return result.getData(); } log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}", tenantId, accountNo); - R saveResult = userClient.saveUser(buildIamUser(profileResponse, tenantId, accountNo)); + R saveResult = userClient.saveIamUser(buildIamUser(profileResponse, tenantId, accountNo)); if (!saveResult.isSuccess() || !Boolean.TRUE.equals(saveResult.getData())) { log.warn("IAM统一身份认证自动创建用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, saveResult.getMsg()); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); @@ -185,12 +187,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { user.setTenantId(tenantId); user.setUserType(UserType.WEB.getCategory()); user.setAccount(accountNo); - user.setPassword(accountNo); + user.setPassword(UUID.randomUUID().toString()); user.setName(accountNo); user.setRealName(accountNo); user.setRoleId(StringPool.MINUS_ONE); user.setDeptId(StringPool.MINUS_ONE); user.setPostId(StringPool.MINUS_ONE); + user.setIsOa(1); + user.setSyncTime(new Date()); user.setStatus(StatusType.ACTIVE.getType()); log.info("IAM统一身份认证自动创建用户参数,tenantId={}, accountNo={}, iamId={}", tenantId, accountNo, profileResponse.getId()); return user; @@ -206,13 +210,13 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { } private boolean isIamRequest(OAuth2Request request, boolean logMiss) { - String redirectUri = normalizeRedirectUri(request.getRedirectUri()); - boolean iamRequest = StringUtil.isNotBlank(properties.getRedirectUri()) + String redirectUri = resolveRedirectUri(request); + boolean iamRequest = StringUtil.isNotBlank(redirectUri) && StringUtil.isNotBlank(request.getCode()) - && StringUtil.equals(properties.getRedirectUri(), redirectUri); + && isDedicatedIamEndpoint(request); if (!iamRequest && logMiss) { - log.info("IAM统一身份认证请求未命中,configRedirectUri={}, requestRedirectUri={}, normalizedRedirectUri={}, code={}", - properties.getRedirectUri(), request.getRedirectUri(), redirectUri, request.getCode()); + log.info("IAM统一身份认证请求未命中,configRedirectUri={}, requestRedirectUri={}, normalizedRedirectUri={}, hasCode={}", + properties.getRedirectUri(), request.getRedirectUri(), redirectUri, StringUtil.isNotBlank(request.getCode())); } return iamRequest; } @@ -247,8 +251,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { properties.getTokenUrl(), properties.getProfileUrl(), properties.getClientId(), - properties.getClientSecret(), - properties.getRedirectUri() + properties.getClientSecret() )) { throw new UserInvalidException("IAM统一身份认证配置不完整"); } @@ -265,7 +268,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { .header(HttpHeaders.AUTHORIZATION, authorizationHeader()) .POST(HttpRequest.BodyPublishers.ofString(tokenBody, StandardCharsets.UTF_8)) .build(); - log.info("IAM统一身份认证换取Token请求,method={}, headers={}, body={}", httpRequest.method(), httpRequest.headers().map(), tokenBody); + log.info("IAM统一身份认证换取Token请求,url={}, method={}", properties.getTokenUrl(), httpRequest.method()); HttpResponse response = httpClient.send( httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8) @@ -292,12 +295,12 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { .header(HttpHeaders.AUTHORIZATION, profileAuthorizationHeader(accessToken)) .GET() .build(); - log.info("IAM统一身份认证获取用户信息请求,headers={}, body={}", httpRequest.headers().map(), "无"); + log.info("IAM统一身份认证获取用户信息请求,url={}, method={}", properties.getProfileUrl(), httpRequest.method()); HttpResponse response = httpClient.send( httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8) ); - log.info("IAM统一身份认证获取用户信息响应,headers={}, body={}", response.headers().map(), response.body()); + log.info("IAM统一身份认证获取用户信息响应,status={}", response.statusCode()); if (response.statusCode() < 200 || response.statusCode() >= 300) { log.warn("IAM统一身份认证获取用户信息失败,status={}", response.statusCode()); throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT); @@ -321,10 +324,24 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { if (StringUtil.isNotBlank(request.getState())) { params.put("state", request.getState()); } - params.put("redirect_uri", properties.getRedirectUri()); + params.put("redirect_uri", resolveRedirectUri(request)); return params; } + /** + * 获取 IAM 回调地址,优先使用前端请求传入的值,兼容未传参时的后端默认配置。 + * + * @param request OAuth2 请求 + * @return IAM 回调地址 + */ + private String resolveRedirectUri(OAuth2Request request) { + String requestRedirectUri = normalizeRedirectUri(request.getRedirectUri()); + if (StringUtil.isNotBlank(requestRedirectUri)) { + return requestRedirectUri; + } + return normalizeRedirectUri(properties.getRedirectUri()); + } + private String buildTokenBody(Map params) { return params.entrySet().stream() .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue())) diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index ae6f2c9..586debc 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -89,3 +89,16 @@ social: client-id: 233************ client-secret: 233************************************ redirect-uri: ${social.domain}/oauth/redirect/dingtalk + +# IAM统一身份认证 +iam: + sso: + token-url: ${IAM_SSO_TOKEN_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_SSO_TOKEN} + profile-url: ${IAM_SSO_PROFILE_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_SSO_PROFILE} + client-id: ${IAM_SSO_CLIENT_ID:f0b52f23f71b1649c468} + client-secret: ${IAM_SSO_CLIENT_SECRET:5b3d8575f0899946623ab86523ac3dd8ee98} + system-client-id: ${IAM_SSO_SYSTEM_CLIENT_ID:saber3} + system-client-secret: ${IAM_SSO_SYSTEM_CLIENT_SECRET:saber3_secret} + redirect-uri: ${IAM_SSO_REDIRECT_URI:http://172.16.203.228:8000/callback} + authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} 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 9520b05..86787cf 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 @@ -61,6 +61,7 @@ public class AuthProvider { DEFAULT_SKIP_URL.add("/process/diagram-view"); DEFAULT_SKIP_URL.add("/manager/check-upload"); DEFAULT_SKIP_URL.add("/assets/**"); + DEFAULT_SKIP_URL.add("/iam/sso/token/**"); } /** diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java index 97020dd..a042452 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java @@ -70,6 +70,7 @@ public interface ISysClient { String PARAM = API_PREFIX + "/param"; String PARAM_VALUE = API_PREFIX + "/param-value"; String REGION = API_PREFIX + "/region"; + String FEE_ITEMS = API_PREFIX + "/fee-items"; /** * 获取菜单 @@ -292,4 +293,12 @@ public interface ISysClient { @GetMapping(REGION) R getRegion(@RequestParam("code") String code); + /** + * 获取启用的费用项 + * + * @return 费用项集合 + */ + @GetMapping(FEE_ITEMS) + R> getFeeItems(); + } diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java index 47e40d7..fe7d1d0 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java @@ -159,5 +159,10 @@ public class ISysClientFallback implements ISysClient { return R.fail("获取数据失败"); } + @Override + public R> getFeeItems() { + return R.fail("获取数据失败"); + } + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java new file mode 100644 index 0000000..b8d873d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java @@ -0,0 +1,23 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** 正式结算付款申请请求。 @author Chill */ +@Data +public class FormalSettlementPaymentRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private BigDecimal appliedAmount; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java new file mode 100644 index 0000000..5bb3c82 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -0,0 +1,31 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** 正式结算保存请求。 @author Chill */ +@Data +public class FormalSettlementSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long contractId; + private String settlementType; + private List sourcePreSettlementIds; + private List sourceDetailIds; + private LocalDate exchangeRateDate; + private BigDecimal exchangeRate; + private String attachmentsJson; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java new file mode 100644 index 0000000..e48fac7 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java @@ -0,0 +1,21 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +/** 正式结算状态操作请求。 @author Chill */ +@Data +public class FormalSettlementStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java new file mode 100644 index 0000000..9827a15 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java @@ -0,0 +1,40 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 预结算预付申请请求 + * + * @author Chill + */ +@Data +@Schema(description = "预结算预付申请请求") +public class PreSettlementAdvanceRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "申请预付金额") + private BigDecimal appliedAmount; + + @Schema(description = "金蝶预付单号") + private String kingdeeAdvanceNo; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java new file mode 100644 index 0000000..bbfddec --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java @@ -0,0 +1,77 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 预结算明细调整请求 + * + * @author Chill + */ +@Data +@Schema(description = "预结算明细调整请求") +public class PreSettlementDetailAdjustRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算明细ID") + private Long detailId; + + @Schema(description = "调整原因") + private String changeReason; + + @Schema(description = "明细费用行") + private List rows; + + @Data + @Schema(description = "明细费用行") + public static class FeeRow implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "费用快照ID") + private Long id; + + @Schema(description = "运输总量") + private BigDecimal transportQuantity; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "运输单价") + private BigDecimal unitPrice; + + @Schema(description = "运费") + private BigDecimal freightAmount; + + @Schema(description = "费用项目") + private Map feeItems; + + @Schema(description = "结算金额(含税)") + private BigDecimal settlementAmountTax; + + @Schema(description = "结算金额(不含税)") + private BigDecimal settlementAmountNoTax; + + @Schema(description = "备注") + private String remark; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java new file mode 100644 index 0000000..1140702 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java @@ -0,0 +1,99 @@ +/** + * 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.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * 预结算单保存请求 + * + * @author Chill + */ +@Data +@Schema(description = "预结算单保存请求") +public class PreSettlementSaveRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long id; + + @Schema(description = "合同ID") + private Long contractId; + + @Schema(description = "汇率日期") + private LocalDate exchangeRateDate; + + @Schema(description = "结算汇率") + private BigDecimal exchangeRate; + + @Schema(description = "附件JSON") + private String attachmentsJson; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "应收应付明细ID") + private List sourceDetailIds; + + @Schema(description = "结算合计费用") + private List summaryFees; + + @Data + @Schema(description = "结算合计费用") + public static class SummaryFee implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "合计费用ID") + private Long id; + + @Schema(description = "费用类型") + private String feeType; + + @Schema(description = "费用项") + private String feeItem; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否手工添加") + private Integer manualFlag; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.java new file mode 100644 index 0000000..3d30ff6 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.java @@ -0,0 +1,42 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 预结算状态操作请求 + * + * @author Chill + */ +@Data +@Schema(description = "预结算状态操作请求") +public class PreSettlementStatusRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long id; + + @Schema(description = "操作原因") + private String reason; + + @Schema(description = "当前节点") + private String currentNode; + + @Schema(description = "当前处理人") + private String currentProcessor; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java new file mode 100644 index 0000000..29e9a11 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java @@ -0,0 +1,65 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 正式结算单实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement") +@Schema(description = "正式结算单") +public class FormalSettlement extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String formalSettlementNo; + private String sourceType; + private String settlementType; + private Long projectId; + private String projectName; + private Long deptId; + private String deptName; + private Long contractId; + private String contractNo; + private String contractName; + private String payerName; + private String payeeName; + private String currency; + private BigDecimal settlementAmount; + private String localCurrency; + private BigDecimal localSettlementAmount; + private BigDecimal appliedPaymentAmount; + private BigDecimal paidAmount; + private LocalDate exchangeRateDate; + private BigDecimal exchangeRate; + private String invoiceStatus; + private String paymentStatus; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String kingdeeBillNo; + private String kingdeeSyncStatus; + private String attachmentsJson; + private String remark; + private LocalDateTime approvedTime; + private LocalDateTime syncedTime; + private String voidReason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java new file mode 100644 index 0000000..4aedce4 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java @@ -0,0 +1,59 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 正式结算明细快照实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_detail") +public class FormalSettlementDetail extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private Long sourcePreSettlementId; + private Long sourcePreSettlementDetailId; + private Long sourceDetailId; + private Integer lineNo; + private String documentNo; + private Long waybillId; + private String waybillNo; + private String vehicleNo; + private String departureAddress; + private String arrivalAddress; + private LocalDateTime actualDepartureTime; + private LocalDateTime actualCompletionTime; + private String transportType; + private String cargoName; + private String cargoType; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private String batchNo; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal originalAmount; + private BigDecimal adjustAmount; + private BigDecimal settlementAmountTax; + private BigDecimal settlementAmountNoTax; + private String currency; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java new file mode 100644 index 0000000..b7e5892 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java @@ -0,0 +1,45 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算货物费用快照实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_detail_fee") +public class FormalSettlementDetailFee extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementDetailId; + private Long sourceFeeId; + private String lineNo; + private String cargoName; + private String cargoType; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal originalAmount; + private BigDecimal adjustAmount; + private BigDecimal settlementAmountTax; + private BigDecimal settlementAmountNoTax; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java new file mode 100644 index 0000000..962f7ad --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java @@ -0,0 +1,37 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算付款申请实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_payment") +public class FormalSettlementPayment extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private String paymentNo; + private String paymentType; + private BigDecimal appliedAmount; + private BigDecimal paidAmount; + private String billStatus; + private String kingdeeBillNo; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java new file mode 100644 index 0000000..1f0569b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java @@ -0,0 +1,35 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算来源预结算实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_source") +public class FormalSettlementSource extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private Long preSettlementId; + private String preSettlementNo; + private BigDecimal settlementAmount; + private BigDecimal advanceAppliedAmount; + private BigDecimal advancePaidAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java new file mode 100644 index 0000000..7181170 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java @@ -0,0 +1,143 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 预结算单实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement") +@Schema(description = "预结算单") +public class PreSettlement extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单号") + private String preSettlementNo; + + @Schema(description = "来源") + private String sourceType; + + @Schema(description = "结算类型:receivable/payable") + private String settlementType; + + @Schema(description = "项目ID") + private Long projectId; + + @Schema(description = "项目名称") + private String projectName; + + @Schema(description = "所属组织ID") + private Long deptId; + + @Schema(description = "所属组织") + private String deptName; + + @Schema(description = "合同ID") + private Long contractId; + + @Schema(description = "合同编号") + private String contractNo; + + @Schema(description = "合同名称") + private String contractName; + + @Schema(description = "付款方") + private String payerName; + + @Schema(description = "收款方") + private String payeeName; + + @Schema(description = "结算币种") + private String currency; + + @Schema(description = "结算金额") + private BigDecimal settlementAmount; + + @Schema(description = "本位币") + private String localCurrency; + + @Schema(description = "本位币合计") + private BigDecimal localSettlementAmount; + + @Schema(description = "汇率日期") + private LocalDate exchangeRateDate; + + @Schema(description = "结算汇率") + private BigDecimal exchangeRate; + + @Schema(description = "审核状态:draft/reviewing/approved/returned/voided") + private String approvalStatus; + + @Schema(description = "当前节点") + private String currentNode; + + @Schema(description = "当前处理人") + private String currentProcessor; + + @Schema(description = "预付单号") + private String advanceNo; + + @Schema(description = "申请预付金额") + private BigDecimal advanceAppliedAmount; + + @Schema(description = "已付款金额") + private BigDecimal advancePaidAmount; + + @Schema(description = "正式结算单号") + private String formalSettlementNo; + + @Schema(description = "附件JSON") + private String attachmentsJson; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "审核通过时间") + private LocalDateTime approvedTime; + + @Schema(description = "正式结算时间") + private LocalDateTime formalSettledTime; + + @Schema(description = "作废原因") + private String voidReason; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java new file mode 100644 index 0000000..45e2e17 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java @@ -0,0 +1,58 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 预结算预付记录实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_advance") +@Schema(description = "预结算预付记录") +public class PreSettlementAdvance extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "预付单号") + private String advanceNo; + + @Schema(description = "申请预付金额") + private BigDecimal appliedAmount; + + @Schema(description = "已付款金额") + private BigDecimal paidAmount; + + @Schema(description = "单据状态:reviewing/approved/paid/returned/voided") + private String billStatus; + + @Schema(description = "金蝶预付单号") + private String kingdeeAdvanceNo; + + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java new file mode 100644 index 0000000..d1e86ba --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java @@ -0,0 +1,59 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.time.LocalDateTime; + +/** + * 预结算变更记录实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_change_record") +@Schema(description = "预结算变更记录") +public class PreSettlementChangeRecord extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "变更类型") + private String changeType; + + @Schema(description = "行号") + private Integer lineNo; + + @Schema(description = "操作类型") + private String operationType; + + @Schema(description = "变更内容") + private String changeContent; + + @Schema(description = "操作人") + private String operatorName; + + @Schema(description = "变更原因") + private String changeReason; + + @Schema(description = "变更时间") + private LocalDateTime changeTime; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java new file mode 100644 index 0000000..e833aaf --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java @@ -0,0 +1,117 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 预结算明细实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_detail") +@Schema(description = "预结算明细") +public class PreSettlementDetail extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "应收应付明细ID") + private Long sourceDetailId; + + @Schema(description = "行号") + private Integer lineNo; + + @Schema(description = "单据号") + private String documentNo; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "车号") + private String vehicleNo; + + @Schema(description = "发货地址") + private String departureAddress; + + @Schema(description = "到货地址") + private String arrivalAddress; + + @Schema(description = "实际发货时间") + private LocalDateTime actualDepartureTime; + + @Schema(description = "实际完成时间") + private LocalDateTime actualCompletionTime; + + @Schema(description = "运输类型") + private String transportType; + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "货物类型") + private String cargoType; + + @Schema(description = "运输总量") + private BigDecimal transportQuantity; + + @Schema(description = "数量单位") + private String quantityUnit; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "批次号") + private String batchNo; + + @Schema(description = "运输单价") + private BigDecimal unitPrice; + + @Schema(description = "运费") + private BigDecimal freightAmount; + + @Schema(description = "费用项JSON") + private String feeItemsJson; + + @Schema(description = "原金额") + private BigDecimal originalAmount; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "结算金额(含税)") + private BigDecimal settlementAmountTax; + + @Schema(description = "结算金额(不含税)") + private BigDecimal settlementAmountNoTax; + + @Schema(description = "币种") + private String currency; + + @Schema(description = "备注") + private String remark; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java new file mode 100644 index 0000000..3d9dc3b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java @@ -0,0 +1,83 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 预结算明细费用快照实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_detail_fee") +@Schema(description = "预结算明细费用快照") +public class PreSettlementDetailFee extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算明细ID") + private Long preSettlementDetailId; + + @Schema(description = "源费用行ID") + private Long sourceFeeId; + + @Schema(description = "行号") + private String lineNo; + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "货物类型") + private String cargoType; + + @Schema(description = "运输量") + private BigDecimal transportQuantity; + + @Schema(description = "数量单位") + private String quantityUnit; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "运输单价") + private BigDecimal unitPrice; + + @Schema(description = "运费") + private BigDecimal freightAmount; + + @Schema(description = "费用项JSON") + private String feeItemsJson; + + @Schema(description = "原金额") + private BigDecimal originalAmount; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "结算金额(含税)") + private BigDecimal settlementAmountTax; + + @Schema(description = "结算金额(不含税)") + private BigDecimal settlementAmountNoTax; + + @Schema(description = "备注") + private String remark; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java new file mode 100644 index 0000000..aa64ad3 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java @@ -0,0 +1,62 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 预结算合计费用实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_summary_fee") +@Schema(description = "预结算合计费用") +public class PreSettlementSummaryFee extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "行号") + private Integer lineNo; + + @Schema(description = "费用类型") + private String feeType; + + @Schema(description = "费用项") + private String feeItem; + + @Schema(description = "原金额") + private BigDecimal originalAmount; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "结算金额") + private BigDecimal settlementAmount; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否手工添加") + private Integer manualFlag; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java new file mode 100644 index 0000000..8dcd0fd --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -0,0 +1,43 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.time.LocalDate; +import java.util.List; + +/** + * 正式结算单视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FormalSettlementVO extends FormalSettlement { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createStartDate; + @TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createEndDate; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String approvalStatusName; + @TableField(exist = false) private String settlementTypeName; + @TableField(exist = false) private String preSettlementNos; + @TableField(exist = false) private String preSettlementNo; + @TableField(exist = false) private List sources; + @TableField(exist = false) private List details; + @TableField(exist = false) private List payments; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java new file mode 100644 index 0000000..51a4ccd --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java @@ -0,0 +1,108 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementAdvance; +import org.springblade.transport.pojo.entity.PreSettlementChangeRecord; +import org.springblade.transport.pojo.entity.PreSettlementDetail; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * 预结算单视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "预结算单") +public class PreSettlementVO extends PreSettlement { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField(exist = false) + @Schema(description = "生成开始日期") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate createStartDate; + + @TableField(exist = false) + @Schema(description = "生成结束日期") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate createEndDate; + + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + + @TableField(exist = false) + @Schema(description = "更新人姓名") + private String updateUserName; + + @TableField(exist = false) + @Schema(description = "审核状态名称") + private String approvalStatusName; + + @TableField(exist = false) + @Schema(description = "结算类型名称") + private String settlementTypeName; + + @TableField(exist = false) + @Schema(description = "结算明细") + private List details; + + @TableField(exist = false) + @Schema(description = "明细费用快照") + private Map> detailFees; + + @TableField(exist = false) + @Schema(description = "结算合计") + private List summaryFees; + + @TableField(exist = false) + @Schema(description = "预付信息") + private List advances; + + @TableField(exist = false) + @Schema(description = "变更记录") + private List changeRecords; + + @TableField(exist = false) + @Schema(description = "打印模板") + private List> printTemplates; + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java index 3db4bef..9dce693 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java @@ -57,6 +57,7 @@ public interface IUserClient { String USER_BY_ACCOUNT = API_PREFIX + "/user-by-account"; String USER_AUTH_INFO = API_PREFIX + "/user-auth-info"; String SAVE_USER = API_PREFIX + "/save-user"; + String SAVE_IAM_USER = API_PREFIX + "/save-iam-user"; String REGISTER_USER = API_PREFIX + "/register-user"; String REMOVE_USER = API_PREFIX + "/remove-user"; @@ -149,6 +150,15 @@ public interface IUserClient { @PostMapping(SAVE_USER) R saveUser(@RequestBody User user); + /** + * 新建IAM用户 + * + * @param user 用户实体 + * @return 是否成功 + */ + @PostMapping(SAVE_IAM_USER) + R saveIamUser(@RequestBody User user); + /** * 注册用户 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java index 0c12f90..82d63be 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java @@ -25,6 +25,7 @@ */ package org.springblade.system.feign; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.AllArgsConstructor; import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tool.api.R; @@ -63,6 +64,8 @@ public class SysClient implements ISysClient { private final IRegionService regionService; + private final IFeeItemService feeItemService; + @Override @GetMapping(MENU) public R

getMenu(Long id) { @@ -200,5 +203,14 @@ public class SysClient implements ISysClient { return R.data(regionService.getById(code)); } + @Override + @GetMapping(FEE_ITEMS) + public R> getFeeItems() { + return R.data(feeItemService.list(Wrappers.lambdaQuery() + .eq(FeeItem::getStatus, 1) + .eq(FeeItem::getIsDeleted, 0) + .orderByAsc(FeeItem::getFeeCategory, FeeItem::getName))); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java index ba1ccd4..df55138 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java @@ -108,6 +108,12 @@ public class UserClient implements IUserClient { return R.data(service.submit(user)); } + @Override + @PostMapping(SAVE_IAM_USER) + public R saveIamUser(@RequestBody User user) { + return R.data(service.saveIamUser(user)); + } + @Override @PostMapping(REGISTER_USER) public R registerUser(User user) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java index accb55f..e1ff5ee 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java @@ -280,6 +280,14 @@ public interface IUserService extends BaseService { */ boolean registerUser(User user); + /** + * 新建IAM统一身份认证用户(按可信租户落库) + * + * @param user 用户实体 + * @return 是否成功 + */ + boolean saveIamUser(User user); + /** * 配置用户平台扩展信息(租户守卫校验用户归属) * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index 83ecc2c..944d99e 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -74,6 +74,7 @@ import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -549,6 +550,35 @@ public class UserServiceImpl extends BaseServiceImpl implement return saveUser(user); } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean saveIamUser(User user) { + if (AuthUtil.hasAuth()) { + throw new ServiceException("IAM用户创建仅允许统一身份认证流程调用!"); + } + Tenant tenant = SysCache.getTenant(user.getTenantId()); + if (tenant == null || tenant.getId() == null) { + throw new ServiceException("租户信息错误!"); + } + if (user.getUserType() == null) { + user.setUserType(UserType.WEB.getCategory()); + } + if (StringUtil.isBlank(user.getRoleId())) { + user.setRoleId(StringPool.MINUS_ONE); + } + if (StringUtil.isBlank(user.getDeptId())) { + user.setDeptId(StringPool.MINUS_ONE); + } + if (StringUtil.isBlank(user.getPostId())) { + user.setPostId(StringPool.MINUS_ONE); + } + user.setIsOa(1); + user.setSyncTime(new Date()); + user.setStatus(StatusType.ACTIVE.getType()); + applyUserDefaults(user); + return saveUser(user); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean updatePlatform(Long userId, Integer userType, String userExt) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java new file mode 100644 index 0000000..85b1a0f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -0,0 +1,146 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; +import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; +import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.vo.FormalSettlementVO; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.service.IFormalSettlementService; +import org.springblade.transport.service.IPreSettlementService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * 正式结算单控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "formal_settlement") +@RequestMapping("/formal-settlement") +@Tag(name = "正式结算单", description = "正式结算单管理") +public class FormalSettlementController extends BladeController { + private final IFormalSettlementService formalSettlementService; + private final IPreSettlementService preSettlementService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "正式结算单分页") + public R> list(FormalSettlementVO query, Query pageQuery) { + return R.data(formalSettlementService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "正式结算单详情") + public R detail(@RequestParam Long id) { return R.data(formalSettlementService.detail(id)); } + + @GetMapping("/candidate-pre-settlements") + @ApiOperationSupport(order = 3) + @Operation(summary = "可合并的预结算单") + public R> candidates(PreSettlementVO query, Query pageQuery) { + return R.data(formalSettlementService.candidatePreSettlements(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/contract-options") + @ApiOperationSupport(order = 4) + @Operation(summary = "可选合同") + public R>> contractOptions(@RequestParam(required = false) String keyword) { + return R.data(preSettlementService.contractOptions(keyword)); + } + + @GetMapping("/candidate-details") + @ApiOperationSupport(order = 5) + @Operation(summary = "可选应收应付明细") + public R>> candidateDetails(Query query, @RequestParam Long contractId, + @RequestParam String settlementType, @RequestParam(required = false) String batchNo, + @RequestParam(required = false) String feeStartDate, @RequestParam(required = false) String feeEndDate) { + return R.data(preSettlementService.candidateDetails(Condition.getPage(query), contractId, + settlementType, batchNo, feeStartDate, feeEndDate)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 6) + @Operation(summary = "保存正式结算草稿") + public R save(@RequestBody FormalSettlementSaveRequest request) { return R.data(formalSettlementService.saveDraft(request)); } + + @PostMapping("/remove") + @ApiOperationSupport(order = 7) + @Operation(summary = "删除正式结算草稿") + public R remove(@RequestParam Long id) { formalSettlementService.removeDraft(id); return R.success("删除成功"); } + + @PostMapping("/submit") + @ApiOperationSupport(order = 8) + @Operation(summary = "提交审批") + public R submit(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.submit(request); return R.success("提交成功"); } + + @PostMapping("/approve") + @ApiOperationSupport(order = 9) + @Operation(summary = "审批通过") + public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); return R.success("审批通过"); } + + @PostMapping("/return") + @ApiOperationSupport(order = 10) + @Operation(summary = "审批驳回") + public R returnBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.returnBill(request); return R.success("已驳回"); } + + @PostMapping("/void") + @ApiOperationSupport(order = 11) + @Operation(summary = "作废正式结算单") + public R voidBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.voidBill(request); return R.success("作废成功"); } + + @PostMapping("/sync-kingdee") + @ApiOperationSupport(order = 12) + @Operation(summary = "推送金蝶应付单") + public R syncKingdee(@RequestParam Long id) { return R.data(formalSettlementService.syncKingdee(id)); } + + @GetMapping("/detail-fees") + @ApiOperationSupport(order = 13) + @Operation(summary = "正式结算货物费用快照") + public R> detailFees(@RequestParam Long detailId) { + return R.data(formalSettlementService.detailFees(detailId)); + } + + @PostMapping("/adjust-detail") + @ApiOperationSupport(order = 14) + @Operation(summary = "调整草稿结算明细") + public R adjustDetail(@RequestBody PreSettlementDetailAdjustRequest request) { + formalSettlementService.adjustDetail(request); + return R.success("保存成功"); + } + + @PostMapping("/apply-payment") + @ApiOperationSupport(order = 15) + @Operation(summary = "发起尾款付款申请") + public R applyPayment(@RequestBody FormalSettlementPaymentRequest request) { + return R.data(formalSettlementService.applyPayment(request)); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java new file mode 100644 index 0000000..09ec05a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java @@ -0,0 +1,262 @@ +/** + * 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.transport.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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 lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.excel.util.ExcelUtil; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.transport.excel.PreSettlementExcel; +import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; +import org.springblade.transport.pojo.dto.PreSettlementStatusRequest; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.service.IPreSettlementService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Map; + +/** + * 预结算单控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "pre_settlement") +@RequestMapping("/pre-settlement") +@Tag(name = "预结算单", description = "预结算单管理") +public class PreSettlementController extends BladeController { + + private final IPreSettlementService preSettlementService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "预结算单分页") + public R> list(PreSettlementVO query, Query pageQuery) { + return R.data(preSettlementService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "预结算单详情") + public R detail(@RequestParam Long id) { + return R.data(preSettlementService.detail(id)); + } + + @GetMapping("/contract-options") + @ApiOperationSupport(order = 3) + @Operation(summary = "可选合同") + public R>> contractOptions(@RequestParam(required = false) String keyword) { + return R.data(preSettlementService.contractOptions(keyword)); + } + + @GetMapping("/fee-options") + @ApiOperationSupport(order = 4) + @Operation(summary = "费用类型及费用项") + public R>> feeOptions() { + return R.data(preSettlementService.feeOptions()); + } + + @GetMapping("/candidate-details") + @ApiOperationSupport(order = 5) + @Operation(summary = "可选应收应付明细") + public R>> candidateDetails(Query query, @RequestParam Long contractId, + @RequestParam String settlementType, @RequestParam(required = false) String batchNo, + @RequestParam(required = false) String feeStartDate, + @RequestParam(required = false) String feeEndDate) { + return R.data(preSettlementService.candidateDetails(Condition.getPage(query), contractId, + settlementType, batchNo, feeStartDate, feeEndDate)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 5) + @Operation(summary = "保存预结算草稿") + public R save(@RequestBody PreSettlementSaveRequest request) { + return R.data(preSettlementService.saveDraft(request)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 6) + @Operation(summary = "删除预结算草稿") + public R remove(@RequestParam Long id) { + preSettlementService.removeDraft(id); + return R.success("删除成功"); + } + + @PostMapping("/remove-detail") + @ApiOperationSupport(order = 7) + @Operation(summary = "移除预结算明细") + public R removeDetail(@RequestParam Long id, @RequestParam Long detailId) { + preSettlementService.removeDetail(id, detailId); + return R.success("移除成功"); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 8) + @Operation(summary = "提交预结算审批") + public R submit(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.submit(request); + return R.success("审批流程已发起"); + } + + @PostMapping("/approve") + @ApiOperationSupport(order = 9) + @Operation(summary = "预结算审批通过") + public R approve(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.approve(request); + return R.success("审批通过"); + } + + @PostMapping("/return") + @ApiOperationSupport(order = 10) + @Operation(summary = "预结算审批驳回") + public R returnBill(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.returnBill(request); + return R.success("已驳回"); + } + + @PostMapping("/void") + @ApiOperationSupport(order = 11) + @Operation(summary = "作废预结算单") + public R voidBill(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.voidBill(request); + return R.success("作废成功"); + } + + @PostMapping("/apply-advance") + @ApiOperationSupport(order = 12) + @Operation(summary = "发起预付申请") + public R applyAdvance(@RequestBody PreSettlementAdvanceRequest request) { + preSettlementService.applyAdvance(request); + return R.success("预付申请提交成功"); + } + + @PostMapping("/update-advance-paid") + @ApiOperationSupport(order = 13) + @Operation(summary = "回写预付付款金额") + public R updateAdvancePaid(@RequestParam Long advanceId, @RequestParam BigDecimal paidAmount, + @RequestParam(required = false) String kingdeeAdvanceNo) { + preSettlementService.updateAdvancePaidAmount(advanceId, paidAmount, kingdeeAdvanceNo); + return R.success("付款金额更新成功"); + } + + @PostMapping("/void-advance") + @ApiOperationSupport(order = 14) + @Operation(summary = "作废预付申请") + public R voidAdvance(@RequestParam Long advanceId, @RequestParam(required = false) String reason) { + preSettlementService.voidAdvance(advanceId, reason); + return R.success("预付申请作废成功"); + } + + @PostMapping("/formal-settlement") + @ApiOperationSupport(order = 16) + @Operation(summary = "尾款结算") + public R formalSettlement(@RequestParam Long id) { + return R.data(preSettlementService.formalSettlement(id)); + } + + @GetMapping("/detail-fees") + @ApiOperationSupport(order = 15) + @Operation(summary = "结算明细费用") + public R> detailFees(@RequestParam Long detailId) { + return R.data(preSettlementService.detailFees(detailId)); + } + + @PostMapping("/adjust-detail") + @ApiOperationSupport(order = 17) + @Operation(summary = "调整结算明细") + public R adjustDetail(@RequestBody PreSettlementDetailAdjustRequest request) { + preSettlementService.adjustDetail(request); + return R.success("保存成功"); + } + + @GetMapping("/print-templates") + @ApiOperationSupport(order = 18) + @Operation(summary = "预结算打印模板") + public R>> printTemplates(@RequestParam Long id) { + return R.data(preSettlementService.printTemplates(id)); + } + + @GetMapping("/export") + @ApiOperationSupport(order = 19) + @Operation(summary = "导出预结算单") + public void export(PreSettlementVO query, HttpServletResponse response) { + IPage page = preSettlementService.selectPage(new Page<>(1, 100000), query); + List rows = page.getRecords().stream().map(this::toExcel).toList(); + ExcelUtil.export(response, "预结算单" + DateUtil.time(), "预结算单", rows, PreSettlementExcel.class); + } + + private PreSettlementExcel toExcel(PreSettlementVO vo) { + PreSettlementExcel excel = new PreSettlementExcel(); + excel.setPreSettlementNo(vo.getPreSettlementNo()); + excel.setSourceType(vo.getSourceType()); + excel.setPayerName(vo.getPayerName()); + excel.setPayeeName(vo.getPayeeName()); + excel.setProjectName(vo.getProjectName()); + excel.setDeptName(vo.getDeptName()); + excel.setContractNo(vo.getContractNo()); + excel.setContractName(vo.getContractName()); + excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency())); + excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency())); + excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString()); + excel.setAdvanceAppliedAmount(formatMoney(vo.getAdvanceAppliedAmount(), vo.getCurrency())); + excel.setAdvancePaidAmount(formatMoney(vo.getAdvancePaidAmount(), vo.getCurrency())); + excel.setApprovalStatusName(vo.getApprovalStatusName()); + excel.setCurrentNode(vo.getCurrentNode()); + excel.setCurrentProcessor(vo.getCurrentProcessor()); + excel.setCreateUserName(vo.getCreateUserName()); + excel.setCreateTime(vo.getCreateTime()); + return excel; + } + + private String formatMoney(BigDecimal value, String currency) { + if (value == null) return ""; + return value.setScale(2, RoundingMode.HALF_UP).toPlainString() + " " + + (currency == null || currency.isBlank() ? "RMB" : currency); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java new file mode 100644 index 0000000..d6d15c0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java @@ -0,0 +1,72 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import cn.idev.excel.annotation.write.style.ContentRowHeight; +import cn.idev.excel.annotation.write.style.HeadRowHeight; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 预结算单 Excel + * + * @author Chill + */ +@Data +@ColumnWidth(22) +@HeadRowHeight(20) +@ContentRowHeight(18) +public class PreSettlementExcel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @ExcelProperty("预结算单号") + private String preSettlementNo; + @ExcelProperty("来源") + private String sourceType; + @ExcelProperty("付款方") + private String payerName; + @ExcelProperty("收款方") + private String payeeName; + @ExcelProperty("项目名称") + private String projectName; + @ExcelProperty("所属组织") + private String deptName; + @ExcelProperty("合同编号") + private String contractNo; + @ExcelProperty("合同名称") + private String contractName; + @ExcelProperty("原币结算金额") + private String settlementAmount; + @ExcelProperty("本位币结算金额") + private String localSettlementAmount; + @ExcelProperty("结算汇率") + private String exchangeRate; + @ExcelProperty("申请预付金额") + private String advanceAppliedAmount; + @ExcelProperty("已付款金额") + private String advancePaidAmount; + @ExcelProperty("审核状态") + private String approvalStatusName; + @ExcelProperty("当前节点") + private String currentNode; + @ExcelProperty("当前处理人") + private String currentProcessor; + @ExcelProperty("创建人") + private String createUserName; + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java new file mode 100644 index 0000000..a0d9ce7 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java @@ -0,0 +1,18 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; + +/** 正式结算货物费用 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementDetailFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java new file mode 100644 index 0000000..121c4af --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java @@ -0,0 +1,18 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; + +/** 正式结算明细 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementDetailMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java new file mode 100644 index 0000000..117950b --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java @@ -0,0 +1,18 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.FormalSettlement; + +/** 正式结算单 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java new file mode 100644 index 0000000..fe41e31 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java @@ -0,0 +1,18 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.FormalSettlementPayment; + +/** 正式结算付款申请 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementPaymentMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java new file mode 100644 index 0000000..793af53 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java @@ -0,0 +1,18 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.FormalSettlementSource; + +/** 正式结算来源 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementSourceMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.java new file mode 100644 index 0000000..c8bb155 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.java @@ -0,0 +1,22 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PreSettlementAdvance; + +/** + * 预结算预付记录 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementAdvanceMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.java new file mode 100644 index 0000000..0d96010 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.java @@ -0,0 +1,22 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PreSettlementChangeRecord; + +/** + * 预结算变更记录 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementChangeRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.java new file mode 100644 index 0000000..7c28dc4 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.java @@ -0,0 +1,22 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; + +/** + * 预结算明细费用 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementDetailFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.java new file mode 100644 index 0000000..143eaaf --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.java @@ -0,0 +1,22 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PreSettlementDetail; + +/** + * 预结算明细 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementDetailMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.java new file mode 100644 index 0000000..c2b8648 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.java @@ -0,0 +1,22 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PreSettlement; + +/** + * 预结算单 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.java new file mode 100644 index 0000000..c587af0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.java @@ -0,0 +1,22 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; + +/** + * 预结算合计费用 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementSummaryFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java new file mode 100644 index 0000000..531d798 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -0,0 +1,43 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; +import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.vo.FormalSettlementVO; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import java.util.List; +import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; + +/** + * 正式结算单服务 + * + * @author Chill + */ +public interface IFormalSettlementService extends BaseService { + IPage selectPage(IPage page, FormalSettlementVO query); + IPage candidatePreSettlements(IPage page, PreSettlementVO query); + FormalSettlementVO detail(Long id); + Long saveDraft(FormalSettlementSaveRequest request); + void removeDraft(Long id); + void submit(FormalSettlementStatusRequest request); + void approve(FormalSettlementStatusRequest request); + void returnBill(FormalSettlementStatusRequest request); + void voidBill(FormalSettlementStatusRequest request); + String syncKingdee(Long id); + List detailFees(Long detailId); + void adjustDetail(PreSettlementDetailAdjustRequest request); + String applyPayment(FormalSettlementPaymentRequest request); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java new file mode 100644 index 0000000..4133a41 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java @@ -0,0 +1,71 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; +import org.springblade.transport.pojo.dto.PreSettlementStatusRequest; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.vo.PreSettlementVO; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 预结算单服务 + * + * @author Chill + */ +public interface IPreSettlementService extends BaseService { + + IPage selectPage(IPage page, PreSettlementVO query); + + PreSettlementVO detail(Long id); + + List> contractOptions(String keyword); + + List> feeOptions(); + + IPage> candidateDetails(IPage page, Long contractId, String settlementType, + String batchNo, String feeStartDate, String feeEndDate); + + Long saveDraft(PreSettlementSaveRequest request); + + void removeDraft(Long id); + + void removeDetail(Long id, Long detailId); + + void submit(PreSettlementStatusRequest request); + + void approve(PreSettlementStatusRequest request); + + void returnBill(PreSettlementStatusRequest request); + + void voidBill(PreSettlementStatusRequest request); + + void applyAdvance(PreSettlementAdvanceRequest request); + + void updateAdvancePaidAmount(Long advanceId, BigDecimal paidAmount, String kingdeeAdvanceNo); + + void voidAdvance(Long advanceId, String reason); + + String formalSettlement(Long id); + + List detailFees(Long detailId); + + void adjustDetail(PreSettlementDetailAdjustRequest request); + + List> printTemplates(Long id); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java new file mode 100644 index 0000000..4eb319f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -0,0 +1,511 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.FormalSettlementDetailMapper; +import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementSourceMapper; +import org.springblade.transport.mapper.FormalSettlementPaymentMapper; +import org.springblade.transport.mapper.PreSettlementDetailMapper; +import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; +import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.ReceivablePayableDetailMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; +import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; +import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementDetail; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.entity.ReceivablePayableDetail; +import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.FormalSettlementVO; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.service.IFormalSettlementService; +import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.wrapper.PreSettlementWrapper; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.transport.wrapper.FormalSettlementWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 正式结算单服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class FormalSettlementServiceImpl extends BaseServiceImpl + implements IFormalSettlementService { + + private static final String DRAFT = "draft"; + private static final String REVIEWING = "reviewing"; + private static final String APPROVED = "approved"; + private static final String RETURNED = "returned"; + private static final String VOIDED = "voided"; + private final FormalSettlementSourceMapper sourceMapper; + private final FormalSettlementPaymentMapper paymentMapper; + private final FormalSettlementDetailMapper detailMapper; + private final FormalSettlementDetailFeeMapper detailFeeMapper; + private final PreSettlementMapper preSettlementMapper; + private final PreSettlementDetailMapper preDetailMapper; + private final PreSettlementDetailFeeMapper preDetailFeeMapper; + private final ReceivablePayableDetailMapper receivablePayableMapper; + private final ReceivablePayableCargoFeeMapper receivablePayableCargoFeeMapper; + private final IContractManageService contractManageService; + private final IWaybillService waybillService; + + @Override + public IPage selectPage(IPage page, FormalSettlementVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getFormalSettlementNo()), FormalSettlement::getFormalSettlementNo, query.getFormalSettlementNo()) + .like(Func.isNotEmpty(query.getProjectName()), FormalSettlement::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), FormalSettlement::getDeptName, query.getDeptName()) + .like(Func.isNotEmpty(query.getContractNo()), FormalSettlement::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getContractName()), FormalSettlement::getContractName, query.getContractName()) + .like(Func.isNotEmpty(query.getPayerName()), FormalSettlement::getPayerName, query.getPayerName()) + .like(Func.isNotEmpty(query.getPayeeName()), FormalSettlement::getPayeeName, query.getPayeeName()) + .eq(Func.isNotEmpty(query.getSettlementType()), FormalSettlement::getSettlementType, query.getSettlementType()) + .eq(Func.isNotEmpty(query.getInvoiceStatus()), FormalSettlement::getInvoiceStatus, query.getInvoiceStatus()) + .eq(Func.isNotEmpty(query.getPaymentStatus()), FormalSettlement::getPaymentStatus, query.getPaymentStatus()) + .eq(Func.isNotEmpty(query.getKingdeeSyncStatus()), FormalSettlement::getKingdeeSyncStatus, query.getKingdeeSyncStatus()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), FormalSettlement::getApprovalStatus, query.getApprovalStatus()) + .ge(query.getCreateStartDate() != null, FormalSettlement::getCreateTime, query.getCreateStartDate() == null ? null : query.getCreateStartDate().atStartOfDay()) + .lt(query.getCreateEndDate() != null, FormalSettlement::getCreateTime, query.getCreateEndDate() == null ? null : query.getCreateEndDate().plusDays(1).atStartOfDay()); + if (Func.isNotEmpty(query.getPreSettlementNo())) { + List ids = sourceMapper.selectList(Wrappers.lambdaQuery() + .like(FormalSettlementSource::getPreSettlementNo, query.getPreSettlementNo())) + .stream().map(FormalSettlementSource::getFormalSettlementId).distinct().toList(); + if (ids.isEmpty()) wrapper.eq(FormalSettlement::getId, -1L); else wrapper.in(FormalSettlement::getId, ids); + } + IPage result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime)); + return result.convert(this::toVO); + } + + @Override + public IPage candidatePreSettlements(IPage page, PreSettlementVO query) { + IPage result = preSettlementMapper.selectPage(page, Wrappers.lambdaQuery() + .eq(PreSettlement::getApprovalStatus, APPROVED) + .and(w -> w.isNull(PreSettlement::getFormalSettlementNo).or().eq(PreSettlement::getFormalSettlementNo, "")) + .eq(query.getContractId() != null, PreSettlement::getContractId, query.getContractId()) + .like(Func.isNotEmpty(query.getPreSettlementNo()), PreSettlement::getPreSettlementNo, query.getPreSettlementNo()) + .like(Func.isNotEmpty(query.getContractNo()), PreSettlement::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getContractName()), PreSettlement::getContractName, query.getContractName()) + .orderByDesc(PreSettlement::getCreateTime)); + return PreSettlementWrapper.build().pageVO(result); + } + + @Override + public FormalSettlementVO detail(Long id) { + FormalSettlement settlement = existing(id); + FormalSettlementVO vo = toVO(settlement); + vo.setSources(sourceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSource::getFormalSettlementId, id).orderByAsc(FormalSettlementSource::getCreateTime))); + vo.setDetails(detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo))); + vo.setPayments(paymentMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime))); + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(FormalSettlementSaveRequest request) { + if (Func.isEmpty(request.getSourcePreSettlementIds()) && Func.isEmpty(request.getSourceDetailIds())) { + throw new ServiceException("请至少选择一张预结算单或一条应收应付明细"); + } + FormalSettlement settlement = request.getId() == null ? new FormalSettlement() : editable(request.getId()); + if (settlement.getId() != null) releaseSources(settlement); + List sources = Func.isEmpty(request.getSourcePreSettlementIds()) ? List.of() + : request.getSourcePreSettlementIds().stream().distinct().map(this::availableSource).toList(); + List directDetails = Func.isEmpty(request.getSourceDetailIds()) ? List.of() + : request.getSourceDetailIds().stream().distinct().map(this::availableDetail).toList(); + Long contractId = sources.isEmpty() ? request.getContractId() : sources.get(0).getContractId(); + String settlementType = sources.isEmpty() ? request.getSettlementType() : sources.get(0).getSettlementType(); + if (contractId == null || Func.isEmpty(settlementType)) throw new ServiceException("请选择合同并确认结算类型"); + if (sources.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()) + || !Objects.equals(settlementType, item.getSettlementType())) + || directDetails.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()) + || !Objects.equals(settlementType, item.getSettlementType()) + || (!sources.isEmpty() && !Objects.equals(sources.get(0).getCurrency(), item.getCurrency())))) { + throw new ServiceException("合并的预结算单必须属于同一合同、结算类型及币种"); + } + PreSettlement first = sources.isEmpty() ? null : sources.get(0); + ContractManage contract = contractManageService.getById(contractId); + if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) throw new ServiceException("合同不存在"); + if (settlement.getId() == null) { + settlement.setFormalSettlementNo(nextNo()); + settlement.setApprovalStatus(DRAFT); + settlement.setCurrentNode("草稿"); + settlement.setSourceType(sources.isEmpty() ? "应收应付" : directDetails.isEmpty() ? "预结算合并" : "混合来源"); + settlement.setInvoiceStatus("unreceived"); + settlement.setPaymentStatus("unpaid"); + settlement.setKingdeeSyncStatus("unsynced"); + } + if (first == null) copyHeader(contract, settlementType, directDetails.get(0), settlement); else copyHeader(first, settlement); + settlement.setExchangeRateDate(request.getExchangeRateDate()); + settlement.setExchangeRate(request.getExchangeRate() == null ? BigDecimal.ONE : positive(request.getExchangeRate(), "结算汇率")); + BigDecimal sourceAmount = sources.stream().map(PreSettlement::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal detailAmount = directDetails.stream().map(ReceivablePayableDetail::getTotalAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + settlement.setSettlementAmount(sourceAmount.add(detailAmount)); + settlement.setAppliedPaymentAmount(sources.stream().map(PreSettlement::getAdvanceAppliedAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + settlement.setPaidAmount(sources.stream().map(PreSettlement::getAdvancePaidAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + settlement.setLocalSettlementAmount(settlement.getSettlementAmount().multiply(settlement.getExchangeRate())); + settlement.setAttachmentsJson(request.getAttachmentsJson()); + settlement.setRemark(limit(request.getRemark(), 200)); + saveOrUpdate(settlement); + rebuildSnapshots(settlement, sources, directDetails); + return settlement.getId(); + } + + @Override @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + FormalSettlement settlement = editable(id); + releaseSources(settlement); + sourceMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, id)); + List detailIds = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, id)).stream().map(FormalSettlementDetail::getId).toList(); + if (!detailIds.isEmpty()) detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)); + detailMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, id)); + removeById(id); + } + + @Override public void submit(FormalSettlementStatusRequest request) { changeStatus(request.getId(), DRAFT, REVIEWING, "财务审核", null); } + @Override public void returnBill(FormalSettlementStatusRequest request) { changeStatus(request.getId(), REVIEWING, RETURNED, "已驳回", request.getReason()); } + + @Override + public void approve(FormalSettlementStatusRequest request) { + FormalSettlement settlement = existing(request.getId()); + if (!REVIEWING.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批中的正式结算单允许审核"); + settlement.setApprovalStatus(APPROVED); + settlement.setCurrentNode("审批通过"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + settlement.setApprovedTime(LocalDateTime.now()); + updateById(settlement); + } + + @Override + public void voidBill(FormalSettlementStatusRequest request) { + FormalSettlement settlement = existing(request.getId()); + if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许作废"); + if ("synced".equals(settlement.getKingdeeSyncStatus())) throw new ServiceException("已同步金蝶的正式结算单不能直接作废"); + settlement.setApprovalStatus(VOIDED); + settlement.setCurrentNode("已作废"); + settlement.setVoidReason(required(limit(request.getReason(), 200), "作废原因")); + updateById(settlement); + } + + @Override + public String syncKingdee(Long id) { + FormalSettlement settlement = existing(id); + if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许同步金蝶"); + if ("synced".equals(settlement.getKingdeeSyncStatus())) return settlement.getKingdeeBillNo(); + String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + settlement.setKingdeeBillNo(kingdeeNo); + settlement.setKingdeeSyncStatus("synced"); + settlement.setSyncedTime(LocalDateTime.now()); + updateById(settlement); + return kingdeeNo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String applyPayment(FormalSettlementPaymentRequest request) { + FormalSettlement settlement = existing(request.getId()); + if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许发起付款申请"); + if (!"payable".equals(settlement.getSettlementType())) throw new ServiceException("仅应付正式结算单允许发起付款申请"); + BigDecimal amount = positive(request.getAppliedAmount(), "申请付款金额"); + BigDecimal available = money(settlement.getSettlementAmount()).subtract(money(settlement.getAppliedPaymentAmount())); + if (amount.compareTo(available) > 0) throw new ServiceException("申请付款金额不能超过剩余可申请金额" + available); + FormalSettlementPayment payment = new FormalSettlementPayment(); + payment.setFormalSettlementId(settlement.getId()); + payment.setPaymentNo(nextPaymentNo()); + payment.setPaymentType("final"); + payment.setAppliedAmount(amount); + payment.setPaidAmount(BigDecimal.ZERO); + payment.setBillStatus(REVIEWING); + payment.setRemark(limit(request.getRemark(), 200)); + paymentMapper.insert(payment); + settlement.setAppliedPaymentAmount(money(settlement.getAppliedPaymentAmount()).add(amount)); + updateById(settlement); + return payment.getPaymentNo(); + } + + @Override + public List detailFees(Long detailId) { + FormalSettlementDetail detail = detailMapper.selectById(detailId); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("正式结算明细不存在"); + return detailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detailId) + .orderByAsc(FormalSettlementDetailFee::getLineNo)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void adjustDetail(PreSettlementDetailAdjustRequest request) { + FormalSettlementDetail detail = detailMapper.selectById(request.getDetailId()); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("正式结算明细不存在"); + FormalSettlement settlement = editable(detail.getFormalSettlementId()); + if (Func.isEmpty(request.getRows())) throw new ServiceException("请填写需要调整的货物费用行"); + String reason = required(limit(request.getChangeReason(), 200), "调整原因"); + Map existing = detailFees(detail.getId()).stream() + .collect(Collectors.toMap(FormalSettlementDetailFee::getId, item -> item)); + if (existing.size() != request.getRows().size()) throw new ServiceException("费用调整行数据不完整"); + for (PreSettlementDetailAdjustRequest.FeeRow row : request.getRows()) { + FormalSettlementDetailFee fee = existing.get(row.getId()); + if (fee == null) throw new ServiceException("存在无效的货物费用行"); + fee.setTransportQuantity(nonNegative(row.getTransportQuantity(), "运输总量")); + fee.setMileage(nonNegative(row.getMileage(), "里程")); + fee.setUnitPrice(nonNegative(row.getUnitPrice(), "运输单价")); + fee.setFreightAmount(nonNegative(row.getFreightAmount(), "运费")); + fee.setFeeItemsJson(JsonUtil.toJson(row.getFeeItems() == null ? java.util.Map.of() : row.getFeeItems())); + fee.setSettlementAmountTax(nonNegative(row.getSettlementAmountTax(), "结算金额(含税)")); + fee.setSettlementAmountNoTax(row.getSettlementAmountNoTax() == null ? null : nonNegative(row.getSettlementAmountNoTax(), "结算金额(不含税)")); + fee.setAdjustAmount(fee.getSettlementAmountTax().subtract(money(fee.getOriginalAmount()))); + fee.setRemark(limit(row.getRemark(), 200)); + detailFeeMapper.updateById(fee); + } + List rows = detailFees(detail.getId()); + detail.setTransportQuantity(rows.stream().map(FormalSettlementDetailFee::getTransportQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setFreightAmount(rows.stream().map(FormalSettlementDetailFee::getFreightAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setOriginalAmount(rows.stream().map(FormalSettlementDetailFee::getOriginalAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setSettlementAmountTax(rows.stream().map(FormalSettlementDetailFee::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setSettlementAmountNoTax(rows.stream().map(FormalSettlementDetailFee::getSettlementAmountNoTax).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(detail.getOriginalAmount())); + detail.setRemark(reason); + detailMapper.updateById(detail); + BigDecimal amount = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream() + .map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + settlement.setSettlementAmount(amount); + settlement.setLocalSettlementAmount(amount.multiply(settlement.getExchangeRate() == null ? BigDecimal.ONE : settlement.getExchangeRate())); + updateById(settlement); + } + + private void rebuildSnapshots(FormalSettlement settlement, List sources, List directDetails) { + List existingDetailIds = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream().map(FormalSettlementDetail::getId).toList(); + if (!existingDetailIds.isEmpty()) detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(FormalSettlementDetailFee::getFormalSettlementDetailId, existingDetailIds)); + sourceMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, settlement.getId())); + detailMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())); + int lineNo = 1; + for (PreSettlement source : sources) { + FormalSettlementSource relation = new FormalSettlementSource(); + relation.setFormalSettlementId(settlement.getId()); relation.setPreSettlementId(source.getId()); + relation.setPreSettlementNo(source.getPreSettlementNo()); relation.setSettlementAmount(source.getSettlementAmount()); + relation.setAdvanceAppliedAmount(source.getAdvanceAppliedAmount()); relation.setAdvancePaidAmount(source.getAdvancePaidAmount()); + sourceMapper.insert(relation); + int reserved = preSettlementMapper.update(null, Wrappers.lambdaUpdate() + .eq(PreSettlement::getId, source.getId()) + .eq(PreSettlement::getApprovalStatus, APPROVED) + .and(w -> w.isNull(PreSettlement::getFormalSettlementNo).or().eq(PreSettlement::getFormalSettlementNo, "")) + .set(PreSettlement::getFormalSettlementNo, settlement.getFormalSettlementNo()) + .set(PreSettlement::getFormalSettledTime, LocalDateTime.now()) + .set(PreSettlement::getCurrentNode, "已锁定(正式结算)")); + if (reserved != 1) throw new ServiceException("预结算单" + source.getPreSettlementNo() + "已被其他正式结算占用"); + for (PreSettlementDetail item : preDetailMapper.selectList(Wrappers.lambdaQuery().eq(PreSettlementDetail::getPreSettlementId, source.getId()))) { + FormalSettlementDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetail.class)); + detail.setId(null); detail.setFormalSettlementId(settlement.getId()); detail.setSourcePreSettlementId(source.getId()); + detail.setSourcePreSettlementDetailId(item.getId()); detail.setLineNo(lineNo++); detailMapper.insert(detail); + copyPreDetailFees(item, detail); + ReceivablePayableDetail original = receivablePayableMapper.selectById(item.getSourceDetailId()); + if (original != null) { original.setFormalSettlementNo(settlement.getFormalSettlementNo()); original.setSettlementStatus("formal_settled"); receivablePayableMapper.updateById(original); } + } + } + for (ReceivablePayableDetail source : directDetails) { + FormalSettlementDetail detail = new FormalSettlementDetail(); + detail.setFormalSettlementId(settlement.getId()); detail.setSourceDetailId(source.getId()); detail.setLineNo(lineNo++); + detail.setDocumentNo(source.getDocumentNo()); detail.setWaybillId(source.getWaybillId()); detail.setWaybillNo(source.getWaybillNo()); + detail.setVehicleNo(source.getVehicleNo()); detail.setTransportType(source.getTransportType()); detail.setCargoName(source.getCargoName()); + detail.setCargoType(source.getCargoType()); detail.setTransportQuantity(source.getTransportQuantity()); detail.setQuantityUnit(source.getQuantityUnit()); + detail.setMileage(source.getMileage()); detail.setBatchNo(source.getBatchNo()); detail.setUnitPrice(source.getUnitPrice()); + detail.setFreightAmount(money(source.getFreightAmount())); detail.setFeeItemsJson(source.getFeeItemsJson()); + detail.setOriginalAmount(money(source.getTotalAmount())); detail.setAdjustAmount(BigDecimal.ZERO); + detail.setSettlementAmountTax(money(source.getTotalAmount())); detail.setCurrency(Func.isEmpty(source.getCurrency()) ? "RMB" : source.getCurrency()); + Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId()); + if (waybill != null) { + detail.setDepartureAddress(Func.isNotEmpty(waybill.getDepartureAddress()) ? waybill.getDepartureAddress() : waybill.getDepartureName()); + detail.setArrivalAddress(Func.isNotEmpty(waybill.getArrivalAddress()) ? waybill.getArrivalAddress() : waybill.getArrivalName()); + detail.setActualDepartureTime(waybill.getStartDate() == null ? null : waybill.getStartDate().atStartOfDay()); + detail.setActualCompletionTime(waybill.getEndDate() == null ? null : waybill.getEndDate().atStartOfDay()); + } + detail.setRemark(source.getRemark()); detailMapper.insert(detail); + copyDirectDetailFees(source, detail); + int affected = receivablePayableMapper.update(null, Wrappers.lambdaUpdate() + .eq(ReceivablePayableDetail::getId, source.getId()) + .eq(ReceivablePayableDetail::getSettlementStatus, "pending") + .and(w -> w.isNull(ReceivablePayableDetail::getPreSettlementNo).or().eq(ReceivablePayableDetail::getPreSettlementNo, "")) + .and(w -> w.isNull(ReceivablePayableDetail::getFormalSettlementNo).or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")) + .set(ReceivablePayableDetail::getFormalSettlementNo, settlement.getFormalSettlementNo()) + .set(ReceivablePayableDetail::getSettlementStatus, "formal_settled")); + if (affected != 1) throw new ServiceException("单据" + source.getDocumentNo() + "已被其他结算单选择"); + } + } + + private void copyPreDetailFees(PreSettlementDetail source, FormalSettlementDetail target) { + List fees = preDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementDetailFee::getPreSettlementDetailId, source.getId()).orderByAsc(PreSettlementDetailFee::getLineNo)); + for (PreSettlementDetailFee item : fees) { + FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetailFee.class)); + fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setSourceFeeId(item.getId()); detailFeeMapper.insert(fee); + } + if (fees.isEmpty()) createSingleFee(target); + } + + private void copyDirectDetailFees(ReceivablePayableDetail source, FormalSettlementDetail target) { + List fees = receivablePayableCargoFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getDetailId, source.getId()).orderByAsc(ReceivablePayableCargoFee::getLineNo)); + for (ReceivablePayableCargoFee item : fees) { + FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetailFee.class)); + fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setSourceFeeId(item.getId()); + fee.setSettlementAmountTax(item.getAfterAmount() == null ? money(item.getOriginalAmount()) : item.getAfterAmount()); + fee.setSettlementAmountNoTax(null); detailFeeMapper.insert(fee); + } + if (fees.isEmpty()) createSingleFee(target); + } + + private void createSingleFee(FormalSettlementDetail target) { + FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(target, FormalSettlementDetailFee.class)); + fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setLineNo("0001"); detailFeeMapper.insert(fee); + } + + private void releaseSources(FormalSettlement settlement) { + for (FormalSettlementSource relation : sourceMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, settlement.getId()))) { + PreSettlement source = preSettlementMapper.selectById(relation.getPreSettlementId()); + if (source != null && Objects.equals(source.getFormalSettlementNo(), settlement.getFormalSettlementNo())) { + preSettlementMapper.update(null, Wrappers.lambdaUpdate() + .eq(PreSettlement::getId, source.getId()) + .set(PreSettlement::getFormalSettlementNo, null) + .set(PreSettlement::getFormalSettledTime, null) + .set(PreSettlement::getCurrentNode, "审批通过")); + } + } + for (FormalSettlementDetail detail : detailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId()))) { + ReceivablePayableDetail source = receivablePayableMapper.selectById(detail.getSourceDetailId()); + if (source != null && Objects.equals(source.getFormalSettlementNo(), settlement.getFormalSettlementNo())) { + receivablePayableMapper.update(null, Wrappers.lambdaUpdate() + .eq(ReceivablePayableDetail::getId, source.getId()) + .set(ReceivablePayableDetail::getFormalSettlementNo, null) + .set(ReceivablePayableDetail::getSettlementStatus, + detail.getSourcePreSettlementId() == null ? "pending" : "pre_settled")); + } + } + } + + private FormalSettlementVO toVO(FormalSettlement entity) { + FormalSettlementVO vo = FormalSettlementWrapper.build().entityVO(entity); + vo.setPreSettlementNos(sourceMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, entity.getId())).stream().map(FormalSettlementSource::getPreSettlementNo).collect(Collectors.joining(","))); + return vo; + } + + private PreSettlement availableSource(Long id) { + PreSettlement source = preSettlementMapper.selectById(id); + if (source == null || Objects.equals(source.getIsDeleted(), 1)) throw new ServiceException("预结算单不存在"); + if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("仅审批通过的预结算单可生成正式结算"); + if (Func.isNotEmpty(source.getFormalSettlementNo())) throw new ServiceException("预结算单" + source.getPreSettlementNo() + "已被正式结算占用"); + return source; + } + + private ReceivablePayableDetail availableDetail(Long id) { + ReceivablePayableDetail detail = receivablePayableMapper.selectById(id); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("应收应付明细不存在"); + if (!"pending".equals(detail.getSettlementStatus()) || Func.isNotEmpty(detail.getPreSettlementNo()) || Func.isNotEmpty(detail.getFormalSettlementNo())) { + throw new ServiceException("单据" + detail.getDocumentNo() + "已被结算或关闭"); + } + return detail; + } + + private FormalSettlement existing(Long id) { + FormalSettlement entity = getById(id); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("正式结算单不存在"); + return entity; + } + + private FormalSettlement editable(Long id) { + FormalSettlement entity = existing(id); + if (!DRAFT.equals(entity.getApprovalStatus()) && !RETURNED.equals(entity.getApprovalStatus())) throw new ServiceException("仅草稿或已驳回的正式结算单允许编辑"); + return entity; + } + + private void changeStatus(Long id, String expected, String target, String node, String reason) { + FormalSettlement settlement = existing(id); + if (!expected.equals(settlement.getApprovalStatus()) && !(DRAFT.equals(expected) && RETURNED.equals(settlement.getApprovalStatus()))) throw new ServiceException("当前状态不允许该操作"); + settlement.setApprovalStatus(target); settlement.setCurrentNode(node); settlement.setCurrentProcessor(AuthUtil.getUserName()); + if (RETURNED.equals(target)) settlement.setVoidReason(limit(reason, 200)); + updateById(settlement); + } + + private void copyHeader(PreSettlement source, FormalSettlement target) { + target.setSettlementType(source.getSettlementType()); target.setProjectId(source.getProjectId()); target.setProjectName(source.getProjectName()); + target.setDeptId(source.getDeptId()); target.setDeptName(source.getDeptName()); target.setContractId(source.getContractId()); target.setContractNo(source.getContractNo()); + target.setContractName(source.getContractName()); target.setPayerName(source.getPayerName()); target.setPayeeName(source.getPayeeName()); + target.setCurrency(source.getCurrency()); target.setLocalCurrency(source.getLocalCurrency()); + } + + private void copyHeader(ContractManage contract, String settlementType, ReceivablePayableDetail source, FormalSettlement target) { + target.setSettlementType(settlementType); target.setProjectId(contract.getProjectId()); target.setProjectName(contract.getProjectName()); + target.setDeptId(contract.getOrganizationId()); target.setDeptName(contract.getOrganizationName()); target.setContractId(contract.getId()); + target.setContractNo(contract.getContractNo()); target.setContractName(contract.getContractName()); + if ("receivable".equals(settlementType)) { target.setPayerName(source.getCustomerName()); target.setPayeeName(contract.getPartyA()); } + else { target.setPayerName(contract.getPartyA()); target.setPayeeName(source.getCustomerName()); } + target.setCurrency(Func.isEmpty(source.getCurrency()) ? "RMB" : source.getCurrency()); target.setLocalCurrency("RMB"); + } + + private synchronized String nextNo() { + String prefix = "JS" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + long count = count(Wrappers.lambdaQuery().likeRight(FormalSettlement::getFormalSettlementNo, prefix)); + return prefix + String.format("%04d", count + 1); + } + + private synchronized String nextPaymentNo() { + String prefix = "FK" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + long count = paymentMapper.selectCount(Wrappers.lambdaQuery() + .likeRight(FormalSettlementPayment::getPaymentNo, prefix)); + return prefix + String.format("%04d", count + 1); + } + + private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.signum() < 0) throw new ServiceException(field + "不能小于0"); return value; } + private BigDecimal positive(BigDecimal value, String field) { if (value == null || value.signum() <= 0) throw new ServiceException(field + "必须大于0"); return value; } + private String required(String value, String field) { if (Func.isEmpty(value)) throw new ServiceException("请填写" + field); return value; } + private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("内容不能超过" + max + "个字"); return value; } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java new file mode 100644 index 0000000..e90abaf --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -0,0 +1,1404 @@ +/** + * 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.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.PreSettlementAdvanceMapper; +import org.springblade.transport.mapper.PreSettlementChangeRecordMapper; +import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; +import org.springblade.transport.mapper.PreSettlementDetailMapper; +import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +import org.springblade.transport.mapper.ReceivablePayableDetailMapper; +import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; +import org.springblade.transport.pojo.dto.PreSettlementStatusRequest; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementAdvance; +import org.springblade.transport.pojo.entity.PreSettlementChangeRecord; +import org.springblade.transport.pojo.entity.PreSettlementDetail; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; +import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; +import org.springblade.transport.pojo.entity.ReceivablePayableDetail; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.service.ICustomerArchiveService; +import org.springblade.transport.service.IPreSettlementService; +import org.springblade.transport.service.IProjectApplyService; +import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.wrapper.PreSettlementWrapper; +import org.springblade.system.cache.UserCache; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.pojo.entity.DictBiz; +import org.springblade.system.pojo.entity.FeeItem; +import org.springblade.system.cache.DictBizCache; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 预结算单服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class PreSettlementServiceImpl extends BaseServiceImpl + implements IPreSettlementService { + + private static final String STATUS_DRAFT = "draft"; + private static final String STATUS_REVIEWING = "reviewing"; + private static final String STATUS_APPROVED = "approved"; + private static final String STATUS_RETURNED = "returned"; + private static final String STATUS_VOIDED = "voided"; + private static final String LOCAL_CURRENCY = "RMB"; + + private final PreSettlementDetailMapper detailMapper; + private final PreSettlementDetailFeeMapper detailFeeMapper; + private final PreSettlementSummaryFeeMapper summaryFeeMapper; + private final PreSettlementAdvanceMapper advanceMapper; + private final PreSettlementChangeRecordMapper changeRecordMapper; + private final ReceivablePayableDetailMapper sourceDetailMapper; + private final ReceivablePayableCargoFeeMapper sourceFeeMapper; + private final IContractManageService contractManageService; + private final IProjectApplyService projectApplyService; + private final ICustomerArchiveService customerArchiveService; + private final IWaybillService waybillService; + private final ISysClient sysClient; + + @Override + public IPage selectPage(IPage page, PreSettlementVO query) { + return PreSettlementWrapper.build().pageVO(page(page, buildQuery(query))); + } + + @Override + public List> feeOptions() { + Map categoryNames = DictBizCache.getList("fee_category").stream() + .collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first, + LinkedHashMap::new)); + org.springblade.core.tool.api.R> response = sysClient.getFeeItems(); + if (response == null || !response.isSuccess() || response.getData() == null) { + throw new ServiceException("费用项基础档案读取失败,请稍后重试"); + } + return response.getData().stream() + .collect(Collectors.groupingBy(FeeItem::getFeeCategory, LinkedHashMap::new, Collectors.toList())) + .entrySet().stream().map(entry -> { + Map option = new LinkedHashMap<>(); + option.put("feeType", entry.getKey()); + option.put("feeTypeName", categoryNames.getOrDefault(entry.getKey(), entry.getKey())); + option.put("feeItems", entry.getValue().stream().map(FeeItem::getName).distinct().toList()); + return option; + }).toList(); + } + + @Override + public PreSettlementVO detail(Long id) { + PreSettlement settlement = loadExisting(id); + PreSettlementVO vo = PreSettlementWrapper.build().entityVO(settlement); + List details = listDetails(id); + vo.setDetails(details); + List detailIds = details.stream().map(PreSettlementDetail::getId).toList(); + Map> feeMap = Func.isEmpty(detailIds) ? new HashMap<>() + : detailFeeMapper.selectList(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds) + .eq(PreSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(PreSettlementDetailFee::getCreateTime)) + .stream().collect(Collectors.groupingBy(PreSettlementDetailFee::getPreSettlementDetailId, + LinkedHashMap::new, Collectors.toList())); + vo.setDetailFees(feeMap); + vo.setSummaryFees(listSummaryFees(id)); + List advances = listAdvances(id); + advances.forEach(advance -> advance.setCreateUserName(UserCache.getUserRealName(advance.getCreateUser()))); + vo.setAdvances(advances); + vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementChangeRecord::getPreSettlementId, id) + .eq(PreSettlementChangeRecord::getIsDeleted, 0) + .orderByDesc(PreSettlementChangeRecord::getChangeTime))); + vo.setPrintTemplates(resolvePrintTemplates(settlement)); + return vo; + } + + @Override + public List> contractOptions(String keyword) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .ne(ContractManage::getContractStage, "terminated") + .and(Func.isNotEmpty(keyword), query -> query.like(ContractManage::getContractName, keyword) + .or().like(ContractManage::getContractNo, keyword)) + .orderByDesc(ContractManage::getCreateTime); + return contractManageService.list(wrapper).stream().filter(this::isAvailableContract).map(contract -> { + Map result = new LinkedHashMap<>(); + result.put("id", contract.getId()); + result.put("contractNo", contract.getContractNo()); + result.put("contractName", contract.getContractName()); + result.put("projectId", contract.getProjectId()); + result.put("projectName", contract.getProjectName()); + result.put("deptId", contract.getOrganizationId()); + result.put("deptName", contract.getOrganizationName()); + result.put("partyA", contract.getPartyA()); + result.put("partyB", contract.getPartyB()); + result.put("settlementType", contractSettlementType(contract)); + return result; + }).toList(); + } + + @Override + public IPage> candidateDetails(IPage page, Long contractId, String settlementType, + String batchNo, String feeStartDate, String feeEndDate) { + if (contractId == null) { + throw new ServiceException("请先选择合同"); + } + validateSettlementType(settlementType); + ContractManage contract = loadAvailableContract(contractId); + if (!Objects.equals(contractSettlementType(contract), settlementType)) { + throw new ServiceException("结算类型与合同类别不一致"); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(ReceivablePayableDetail::getIsDeleted, 0) + .eq(ReceivablePayableDetail::getContractId, contractId) + .eq(ReceivablePayableDetail::getProjectId, contract.getProjectId()) + .eq(ReceivablePayableDetail::getDeptId, contract.getOrganizationId()) + .eq(ReceivablePayableDetail::getSettlementType, settlementType) + .eq(ReceivablePayableDetail::getSettlementStatus, "pending") + .and(query -> query.isNull(ReceivablePayableDetail::getPreSettlementNo) + .or().eq(ReceivablePayableDetail::getPreSettlementNo, "")) + .and(query -> query.isNull(ReceivablePayableDetail::getFormalSettlementNo) + .or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")) + .and(query -> query.eq(ReceivablePayableDetail::getCustomerName, contract.getPartyA()) + .or().eq(ReceivablePayableDetail::getCustomerName, contract.getPartyB())) + .like(Func.isNotEmpty(batchNo), ReceivablePayableDetail::getBatchNo, batchNo) + .ge(Func.isNotEmpty(feeStartDate), ReceivablePayableDetail::getFeeDate, parseDate(feeStartDate)) + .le(Func.isNotEmpty(feeEndDate), ReceivablePayableDetail::getFeeDate, parseDate(feeEndDate)) + .orderByDesc(ReceivablePayableDetail::getCreateTime); + IPage sourcePage = sourceDetailMapper.selectPage( + new Page<>(page.getCurrent(), page.getSize()), wrapper); + Page> result = new Page<>(sourcePage.getCurrent(), sourcePage.getSize(), sourcePage.getTotal()); + result.setRecords(sourcePage.getRecords().stream().map(this::candidateMap).toList()); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(PreSettlementSaveRequest request) { + if (request.getContractId() == null) { + throw new ServiceException("请选择合同名称"); + } + if (request.getRemark() != null && request.getRemark().length() > 200) { + throw new ServiceException("备注不能超过200个字符"); + } + ContractManage contract = loadAvailableContract(request.getContractId()); + PreSettlement settlement = request.getId() == null ? new PreSettlement() : loadEditable(request.getId()); + boolean created = settlement.getId() == null; + List existingDetails = created ? List.of() : listDetails(settlement.getId()); + boolean contractChanged = !created && !Objects.equals(settlement.getContractId(), contract.getId()); + if (contractChanged && !existingDetails.isEmpty()) { + throw new ServiceException("预结算单已存在结算明细,不能更换合同"); + } + if (contractChanged) { + settlement.setSettlementType(contractSettlementType(contract)); + settlement.setCurrency(LOCAL_CURRENCY); + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlement.getId())); + } + if (created) { + settlement.setPreSettlementNo(nextPreSettlementNo()); + settlement.setSourceType("应收应付"); + settlement.setSettlementType(resolveSettlementType(request, contract)); + settlement.setApprovalStatus(STATUS_DRAFT); + settlement.setCurrentNode("草稿"); + settlement.setCurrency(LOCAL_CURRENCY); + settlement.setLocalCurrency(LOCAL_CURRENCY); + } + fillContract(settlement, contract); + settlement.setExchangeRateDate(request.getExchangeRateDate()); + settlement.setExchangeRate(request.getExchangeRate()); + settlement.setAttachmentsJson(request.getAttachmentsJson()); + settlement.setRemark(request.getRemark()); + saveOrUpdate(settlement); + if (request.getSourceDetailIds() != null) { + synchronizeDetails(settlement, request.getSourceDetailIds()); + } + settlement.setExchangeRate(normalizeRate(request.getExchangeRate(), settlement.getCurrency())); + rebuildSummaryFees(settlement.getId(), true); + applySummaryRequest(settlement.getId(), contractChanged ? List.of() : request.getSummaryFees()); + refreshSettlementAmount(settlement); + if (created) { + saveChange(settlement.getId(), "结算单基本信息", null, "新增", + "新增预结算单" + settlement.getPreSettlementNo(), ""); + } + return settlement.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + PreSettlement settlement = loadExisting(id); + if (!STATUS_DRAFT.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅草稿状态的预结算单允许删除"); + } + releaseSourceDetails(settlement, listDetails(id)); + deleteChildren(id); + deleteLogic(List.of(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDetail(Long id, Long detailId) { + PreSettlement settlement = loadEditable(id); + PreSettlementDetail detail = detailMapper.selectById(detailId); + if (detail == null || !Objects.equals(detail.getPreSettlementId(), id) + || Objects.equals(detail.getIsDeleted(), 1)) { + throw new ServiceException("预结算明细不存在"); + } + releaseSourceDetails(settlement, List.of(detail)); + detailFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementDetailFee::getPreSettlementDetailId, detailId)); + detailMapper.deleteById(detailId); + saveChange(id, "结算明细项", detail.getLineNo(), "删除", + "删除单据号" + detail.getDocumentNo(), ""); + renumberDetails(id); + rebuildSummaryFees(id, true); + refreshSettlementAmount(settlement); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(PreSettlementStatusRequest request) { + PreSettlement settlement = loadEditable(request.getId()); + validateBeforeSubmit(settlement); + settlement.setApprovalStatus(STATUS_REVIEWING); + settlement.setCurrentNode(Func.isEmpty(request.getCurrentNode()) ? "预结算审批" : request.getCurrentNode()); + settlement.setCurrentProcessor(Func.isEmpty(request.getCurrentProcessor()) ? "待处理" : request.getCurrentProcessor()); + updateById(settlement); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "提交预结算审批", request.getReason()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(PreSettlementStatusRequest request) { + PreSettlement settlement = loadReviewing(request.getId()); + settlement.setApprovalStatus(STATUS_APPROVED); + settlement.setCurrentNode("审批通过"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + settlement.setApprovedTime(LocalDateTime.now()); + updateById(settlement); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批通过", request.getReason()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void returnBill(PreSettlementStatusRequest request) { + PreSettlement settlement = loadReviewing(request.getId()); + String reason = requiredText(limitText(request.getReason(), 200, "驳回原因"), "驳回原因"); + settlement.setApprovalStatus(STATUS_RETURNED); + settlement.setCurrentNode("已驳回"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + updateById(settlement); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批驳回", reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void voidBill(PreSettlementStatusRequest request) { + PreSettlement settlement = loadExisting(request.getId()); + String reason = requiredText(limitText(request.getReason(), 200, "作废原因"), "作废原因"); + if (!STATUS_APPROVED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批通过的预结算单允许作废"); + } + if (Func.isNotEmpty(settlement.getFormalSettlementNo())) { + throw new ServiceException("已转正式结算的预结算单不可作废"); + } + long activeAdvanceCount = advanceMapper.selectCount(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, settlement.getId()) + .eq(PreSettlementAdvance::getIsDeleted, 0) + .ne(PreSettlementAdvance::getBillStatus, STATUS_VOIDED)); + if (activeAdvanceCount > 0) { + throw new ServiceException("该预结算单存在关联预付申请,请先作废预付申请"); + } + settlement.setApprovalStatus(STATUS_VOIDED); + settlement.setCurrentNode("已作废"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + settlement.setVoidReason(reason); + updateById(settlement); + releaseSourceDetails(settlement, listDetails(settlement.getId())); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "作废预结算单", reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applyAdvance(PreSettlementAdvanceRequest request) { + PreSettlement settlement = loadExisting(request.getPreSettlementId()); + if (!STATUS_APPROVED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批通过的应付预结算单允许发起预付申请"); + } + if (!"payable".equals(settlement.getSettlementType())) { + throw new ServiceException("仅应付预结算单允许发起预付申请"); + } + if (Func.isNotEmpty(settlement.getFormalSettlementNo())) { + throw new ServiceException("转正式结算后无法发起预付"); + } + BigDecimal appliedAmount = money(request.getAppliedAmount()); + if (appliedAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("申请预付金额必须大于0"); + } + BigDecimal availableAmount = money(settlement.getSettlementAmount()) + .subtract(money(settlement.getAdvanceAppliedAmount())); + if (appliedAmount.compareTo(availableAmount) > 0) { + throw new ServiceException("申请预付金额不能超过剩余可申请金额"); + } + PreSettlementAdvance advance = new PreSettlementAdvance(); + advance.setPreSettlementId(settlement.getId()); + advance.setAdvanceNo(nextAdvanceNo()); + advance.setAppliedAmount(appliedAmount); + advance.setPaidAmount(BigDecimal.ZERO.setScale(2)); + advance.setBillStatus(STATUS_REVIEWING); + advance.setKingdeeAdvanceNo(limitText(request.getKingdeeAdvanceNo(), 100, "金蝶预付单号")); + advanceMapper.insert(advance); + refreshAdvanceSummary(settlement); + saveChange(settlement.getId(), "预付信息", null, "新增", + "新增预付申请" + advance.getAdvanceNo() + ",申请金额" + appliedAmount, ""); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAdvancePaidAmount(Long advanceId, BigDecimal paidAmount, String kingdeeAdvanceNo) { + PreSettlementAdvance advance = advanceMapper.selectById(advanceId); + if (advance == null || Objects.equals(advance.getIsDeleted(), 1)) { + throw new ServiceException("预付申请不存在"); + } + if (STATUS_VOIDED.equals(advance.getBillStatus())) { + throw new ServiceException("已作废的预付申请不能回写付款金额"); + } + BigDecimal normalizedPaidAmount = money(paidAmount); + if (normalizedPaidAmount.compareTo(BigDecimal.ZERO) < 0 + || normalizedPaidAmount.compareTo(money(advance.getAppliedAmount())) > 0) { + throw new ServiceException("已付款金额必须在0与申请预付金额之间"); + } + advance.setPaidAmount(normalizedPaidAmount); + advance.setKingdeeAdvanceNo(limitText(kingdeeAdvanceNo, 100, "金蝶预付单号")); + advance.setBillStatus(normalizedPaidAmount.compareTo(money(advance.getAppliedAmount())) >= 0 ? "paid" : STATUS_APPROVED); + advanceMapper.updateById(advance); + PreSettlement settlement = loadExisting(advance.getPreSettlementId()); + refreshAdvanceSummary(settlement); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void voidAdvance(Long advanceId, String reason) { + PreSettlementAdvance advance = advanceMapper.selectById(advanceId); + if (advance == null || Objects.equals(advance.getIsDeleted(), 1)) { + throw new ServiceException("预付申请不存在"); + } + if (STATUS_VOIDED.equals(advance.getBillStatus())) { + throw new ServiceException("预付申请已作废,请勿重复操作"); + } + if ("paid".equals(advance.getBillStatus()) || money(advance.getPaidAmount()).compareTo(BigDecimal.ZERO) > 0) { + throw new ServiceException("已付款的预付申请不能作废"); + } + String voidReason = requiredText(limitText(reason, 200, "作废原因"), "作废原因"); + advance.setBillStatus(STATUS_VOIDED); + advanceMapper.updateById(advance); + PreSettlement settlement = loadExisting(advance.getPreSettlementId()); + refreshAdvanceSummary(settlement); + saveChange(settlement.getId(), "预付信息", null, "删除", + "作废预付申请" + advance.getAdvanceNo(), voidReason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String formalSettlement(Long id) { + PreSettlement settlement = loadExisting(id); + if (!STATUS_APPROVED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批通过的预结算单允许尾款结算"); + } + if (Func.isNotEmpty(settlement.getFormalSettlementNo())) { + throw new ServiceException("该预结算单已转正式结算"); + } + if (!"payable".equals(settlement.getSettlementType())) { + throw new ServiceException("仅应付预结算单允许发起尾款结算"); + } + long pendingAdvanceCount = advanceMapper.selectCount(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, id) + .eq(PreSettlementAdvance::getIsDeleted, 0) + .in(PreSettlementAdvance::getBillStatus, STATUS_REVIEWING, STATUS_APPROVED)); + if (pendingAdvanceCount > 0) { + throw new ServiceException("该预结算单存在在途预付申请,无法尾款结算"); + } + String formalSettlementNo = nextFormalSettlementNo(); + settlement.setFormalSettlementNo(formalSettlementNo); + settlement.setFormalSettledTime(LocalDateTime.now()); + settlement.setCurrentNode("已转正式结算"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + updateById(settlement); + for (PreSettlementDetail detail : listDetails(id)) { + ReceivablePayableDetail source = sourceDetailMapper.selectById(detail.getSourceDetailId()); + if (source != null && !Objects.equals(source.getIsDeleted(), 1)) { + source.setFormalSettlementNo(formalSettlementNo); + source.setSettlementStatus("formal_settled"); + sourceDetailMapper.updateById(source); + } + } + saveChange(id, "结算单基本信息", null, "调整", "转正式结算" + formalSettlementNo, ""); + return formalSettlementNo; + } + + @Override + public List detailFees(Long detailId) { + PreSettlementDetail detail = detailMapper.selectById(detailId); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) { + throw new ServiceException("预结算明细不存在"); + } + return detailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementDetailFee::getPreSettlementDetailId, detailId) + .eq(PreSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(PreSettlementDetailFee::getCreateTime)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void adjustDetail(PreSettlementDetailAdjustRequest request) { + PreSettlementDetail detail = detailMapper.selectById(request.getDetailId()); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) { + throw new ServiceException("预结算明细不存在"); + } + PreSettlement settlement = loadEditable(detail.getPreSettlementId()); + if (Func.isEmpty(request.getRows())) { + throw new ServiceException("请填写需要调整的费用行"); + } + String changeReason = requiredText(limitRemark(request.getChangeReason(), 200), "调整原因"); + Map existingMap = detailFees(detail.getId()).stream() + .collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity())); + if (request.getRows().size() != existingMap.size()) { + throw new ServiceException("费用调整行数据不完整"); + } + List changes = new ArrayList<>(); + for (PreSettlementDetailAdjustRequest.FeeRow requestRow : request.getRows()) { + PreSettlementDetailFee fee = existingMap.get(requestRow.getId()); + if (fee == null) { + throw new ServiceException("存在无效的预结算费用行"); + } + BigDecimal beforeQuantity = fee.getTransportQuantity(); + BigDecimal beforeMileage = fee.getMileage(); + BigDecimal beforeUnitPrice = fee.getUnitPrice(); + BigDecimal beforeFreight = fee.getFreightAmount(); + BigDecimal beforeAmount = money(fee.getSettlementAmountTax()); + BigDecimal beforeNoTaxAmount = fee.getSettlementAmountNoTax(); + String beforeRemark = fee.getRemark(); + Map beforeFeeItems = parseFeeItems(fee.getFeeItemsJson()); + fee.setTransportQuantity(nonNegative(requestRow.getTransportQuantity(), "运输总量")); + fee.setMileage(nonNegative(requestRow.getMileage(), "里程")); + fee.setUnitPrice(nonNegative(requestRow.getUnitPrice(), "运输单价")); + fee.setFreightAmount(nonNegative(requestRow.getFreightAmount(), "运费")); + fee.setFeeItemsJson(JsonUtil.toJson(normalizeFeeItems(requestRow.getFeeItems()))); + BigDecimal afterAmount = requestRow.getSettlementAmountTax() == null + ? calculateFeeAmount(fee) : nonNegative(requestRow.getSettlementAmountTax(), "结算金额(含税)"); + fee.setSettlementAmountTax(afterAmount); + fee.setSettlementAmountNoTax(requestRow.getSettlementAmountNoTax() == null ? null + : nonNegative(requestRow.getSettlementAmountNoTax(), "结算金额(不含税)")); + fee.setAdjustAmount(afterAmount.subtract(money(fee.getOriginalAmount()))); + fee.setRemark(limitRemark(requestRow.getRemark(), 200)); + detailFeeMapper.updateById(fee); + String prefix = "【" + firstNotEmpty(fee.getCargoName(), fee.getLineNo()) + "】"; + appendChange(changes, prefix + "运输总量", beforeQuantity, fee.getTransportQuantity()); + appendChange(changes, prefix + "里程", beforeMileage, fee.getMileage()); + appendChange(changes, prefix + "运输单价", beforeUnitPrice, fee.getUnitPrice()); + appendChange(changes, prefix + "运费", beforeFreight, fee.getFreightAmount()); + Map afterFeeItems = parseFeeItems(fee.getFeeItemsJson()); + Set feeItemNames = new LinkedHashSet<>(beforeFeeItems.keySet()); + feeItemNames.addAll(afterFeeItems.keySet()); + feeItemNames.forEach(name -> appendChange(changes, prefix + name, + beforeFeeItems.get(name), afterFeeItems.get(name))); + appendChange(changes, prefix + "结算金额(含税)", beforeAmount, afterAmount); + appendChange(changes, prefix + "结算金额(不含税)", beforeNoTaxAmount, + fee.getSettlementAmountNoTax()); + appendTextChange(changes, prefix + "备注", beforeRemark, fee.getRemark()); + } + if (changes.isEmpty()) { + throw new ServiceException("未修改任何结算明细费用"); + } + refreshDetail(detail); + rebuildSummaryFees(settlement.getId(), true); + refreshSettlementAmount(settlement); + saveChange(settlement.getId(), "结算明细项", detail.getLineNo(), "调整", + String.join(";", changes), changeReason); + } + + @Override + public List> printTemplates(Long id) { + PreSettlement settlement = loadExisting(id); + if (STATUS_VOIDED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("已作废的预结算单不能打印"); + } + return resolvePrintTemplates(settlement); + } + + private LambdaQueryWrapper buildQuery(PreSettlementVO query) { + return Wrappers.lambdaQuery() + .eq(PreSettlement::getIsDeleted, 0) + .like(Func.isNotEmpty(query.getPreSettlementNo()), PreSettlement::getPreSettlementNo, + query.getPreSettlementNo()) + .like(Func.isNotEmpty(query.getAdvanceNo()), PreSettlement::getAdvanceNo, query.getAdvanceNo()) + .like(Func.isNotEmpty(query.getProjectName()), PreSettlement::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), PreSettlement::getDeptName, query.getDeptName()) + .like(Func.isNotEmpty(query.getContractName()), PreSettlement::getContractName, query.getContractName()) + .like(Func.isNotEmpty(query.getContractNo()), PreSettlement::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getPayeeName()), PreSettlement::getPayeeName, query.getPayeeName()) + .like(Func.isNotEmpty(query.getPayerName()), PreSettlement::getPayerName, query.getPayerName()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), PreSettlement::getApprovalStatus, + query.getApprovalStatus()) + .eq(Func.isNotEmpty(query.getSettlementType()), PreSettlement::getSettlementType, + query.getSettlementType()) + .ge(query.getCreateStartDate() != null, PreSettlement::getCreateTime, + query.getCreateStartDate() == null ? null : query.getCreateStartDate().atStartOfDay()) + .le(query.getCreateEndDate() != null, PreSettlement::getCreateTime, + query.getCreateEndDate() == null ? null : query.getCreateEndDate().plusDays(1).atStartOfDay()) + .orderByDesc(PreSettlement::getCreateTime); + } + + private Map candidateMap(ReceivablePayableDetail source) { + Map result = new LinkedHashMap<>(); + result.put("id", source.getId()); + result.put("documentNo", source.getDocumentNo()); + result.put("feeDate", source.getFeeDate()); + result.put("customerName", source.getCustomerName()); + result.put("departureAddress", waybillValue(source.getWaybillId(), Waybill::getDepartureAddress)); + result.put("arrivalAddress", waybillValue(source.getWaybillId(), Waybill::getArrivalAddress)); + result.put("contractName", source.getContractName()); + result.put("waybillNo", source.getWaybillNo()); + result.put("vehicleNo", source.getVehicleNo()); + result.put("transportType", source.getTransportType()); + result.put("cargoName", source.getCargoName()); + result.put("cargoType", source.getCargoType()); + result.put("batchNo", source.getBatchNo()); + result.put("totalAmount", money(source.getTotalAmount())); + result.put("currency", source.getCurrency()); + return result; + } + + private String waybillValue(Long waybillId, Function getter) { + if (waybillId == null) return ""; + Waybill waybill = waybillService.getById(waybillId); + return waybill == null ? "" : getter.apply(waybill); + } + + private void synchronizeDetails(PreSettlement settlement, List requestedIds) { + List distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList(); + List existingDetails = listDetails(settlement.getId()); + Set requestedSet = new LinkedHashSet<>(distinctIds); + List removed = existingDetails.stream() + .filter(detail -> !requestedSet.contains(detail.getSourceDetailId())).toList(); + if (!removed.isEmpty()) { + releaseSourceDetails(settlement, removed); + List removedDetailIds = removed.stream().map(PreSettlementDetail::getId).toList(); + detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, removedDetailIds)); + detailMapper.deleteBatchIds(removedDetailIds); + } + Set existingSourceIds = existingDetails.stream().map(PreSettlementDetail::getSourceDetailId) + .collect(Collectors.toSet()); + List addedIds = distinctIds.stream().filter(id -> !existingSourceIds.contains(id)).toList(); + if (!addedIds.isEmpty()) { + List sources = sourceDetailMapper.selectBatchIds(addedIds); + if (sources.size() != addedIds.size()) { + throw new ServiceException("存在无效的应收应付明细"); + } + boolean hasRetainedDetail = existingDetails.stream() + .anyMatch(detail -> requestedSet.contains(detail.getSourceDetailId())); + for (ReceivablePayableDetail source : sources) { + validateCandidate(settlement, source); + String sourceCurrency = Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency(); + if (!hasRetainedDetail) { + settlement.setCurrency(sourceCurrency); + hasRetainedDetail = true; + } else if (!Objects.equals(settlement.getCurrency(), sourceCurrency)) { + throw new ServiceException("同一预结算单仅允许选择相同币种的结算明细"); + } + PreSettlementDetail detail = copySourceDetail(settlement, source); + detailMapper.insert(detail); + copySourceFees(detail, source); + int affected = sourceDetailMapper.update(null, + Wrappers.lambdaUpdate() + .set(ReceivablePayableDetail::getPreSettlementNo, settlement.getPreSettlementNo()) + .set(ReceivablePayableDetail::getSettlementStatus, "pre_settled") + .eq(ReceivablePayableDetail::getId, source.getId()) + .eq(ReceivablePayableDetail::getSettlementStatus, "pending") + .and(query -> query.isNull(ReceivablePayableDetail::getPreSettlementNo) + .or().eq(ReceivablePayableDetail::getPreSettlementNo, ""))); + if (affected != 1) { + throw new ServiceException("单据" + source.getDocumentNo() + "已被其他预结算单选择"); + } + saveChange(settlement.getId(), "结算明细项", null, "新增", + "新增单据号" + source.getDocumentNo(), ""); + } + updateById(settlement); + } + renumberDetails(settlement.getId()); + } + + private void validateCandidate(PreSettlement settlement, ReceivablePayableDetail source) { + if (!Objects.equals(source.getContractId(), settlement.getContractId())) { + throw new ServiceException("仅可选择当前合同的应收应付明细"); + } + if (!Objects.equals(source.getSettlementType(), settlement.getSettlementType())) { + throw new ServiceException("应收应付明细的结算类型不一致"); + } + if (!Objects.equals(source.getProjectId(), settlement.getProjectId()) + || !Objects.equals(source.getDeptId(), settlement.getDeptId()) + || !List.of(settlement.getPayerName(), settlement.getPayeeName()).contains(source.getCustomerName())) { + throw new ServiceException("应收应付明细的项目、所属组织或客商与预结算单不一致"); + } + if (!"pending".equals(source.getSettlementStatus()) || Func.isNotEmpty(source.getPreSettlementNo()) + || Func.isNotEmpty(source.getFormalSettlementNo())) { + throw new ServiceException("单据" + source.getDocumentNo() + "已被结算或已关闭,不能重复选择"); + } + } + + private PreSettlementDetail copySourceDetail(PreSettlement settlement, ReceivablePayableDetail source) { + PreSettlementDetail detail = new PreSettlementDetail(); + detail.setPreSettlementId(settlement.getId()); + detail.setSourceDetailId(source.getId()); + detail.setDocumentNo(source.getDocumentNo()); + detail.setWaybillId(source.getWaybillId()); + detail.setWaybillNo(source.getWaybillNo()); + detail.setVehicleNo(source.getVehicleNo()); + detail.setTransportType(source.getTransportType()); + detail.setCargoName(source.getCargoName()); + detail.setCargoType(source.getCargoType()); + detail.setTransportQuantity(source.getTransportQuantity()); + detail.setQuantityUnit(source.getQuantityUnit()); + detail.setMileage(source.getMileage()); + detail.setBatchNo(source.getBatchNo()); + detail.setUnitPrice(source.getUnitPrice()); + detail.setFreightAmount(money(source.getFreightAmount())); + detail.setFeeItemsJson(source.getFeeItemsJson()); + detail.setOriginalAmount(money(source.getTotalAmount())); + detail.setAdjustAmount(BigDecimal.ZERO.setScale(2)); + detail.setSettlementAmountTax(money(source.getTotalAmount())); + detail.setCurrency(Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency()); + detail.setRemark(source.getRemark()); + Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId()); + if (waybill != null) { + detail.setDepartureAddress(firstNotEmpty(waybill.getDepartureAddress(), waybill.getDepartureName())); + detail.setArrivalAddress(firstNotEmpty(waybill.getArrivalAddress(), waybill.getArrivalName())); + detail.setActualDepartureTime(waybill.getStartDate() == null ? null : waybill.getStartDate().atStartOfDay()); + detail.setActualCompletionTime(waybill.getEndDate() == null ? null : waybill.getEndDate().atStartOfDay()); + } + return detail; + } + + private void copySourceFees(PreSettlementDetail detail, ReceivablePayableDetail source) { + List sourceFees = sourceFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getDetailId, source.getId()) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .orderByAsc(ReceivablePayableCargoFee::getCreateTime)); + if (sourceFees.isEmpty()) { + PreSettlementDetailFee fee = new PreSettlementDetailFee(); + fee.setPreSettlementDetailId(detail.getId()); + fee.setLineNo("0001"); + fee.setCargoName(source.getCargoName()); + fee.setCargoType(source.getCargoType()); + fee.setTransportQuantity(source.getTransportQuantity()); + fee.setQuantityUnit(source.getQuantityUnit()); + fee.setMileage(source.getMileage()); + fee.setUnitPrice(source.getUnitPrice()); + fee.setFreightAmount(money(source.getFreightAmount())); + fee.setFeeItemsJson(source.getFeeItemsJson()); + fee.setOriginalAmount(money(source.getTotalAmount())); + fee.setAdjustAmount(BigDecimal.ZERO.setScale(2)); + fee.setSettlementAmountTax(money(source.getTotalAmount())); + fee.setRemark(source.getRemark()); + detailFeeMapper.insert(fee); + return; + } + for (ReceivablePayableCargoFee sourceFee : sourceFees) { + PreSettlementDetailFee fee = Objects.requireNonNull( + BeanUtil.copyProperties(sourceFee, PreSettlementDetailFee.class)); + fee.setId(null); + fee.setPreSettlementDetailId(detail.getId()); + fee.setSourceFeeId(sourceFee.getId()); + fee.setSettlementAmountTax(money(sourceFee.getAfterAmount())); + fee.setSettlementAmountNoTax(null); + detailFeeMapper.insert(fee); + } + } + + private void rebuildSummaryFees(Long settlementId, boolean preserveAdjustments) { + PreSettlement settlement = loadExisting(settlementId); + Map feeTypeMap = contractFeeTypeMap(settlement.getContractId()); + List existingRows = listSummaryFees(settlementId); + Map existingGenerated = existingRows.stream() + .filter(row -> !Integer.valueOf(1).equals(row.getManualFlag())) + .collect(Collectors.toMap(this::summaryKey, Function.identity(), (left, right) -> left)); + Map aggregates = new LinkedHashMap<>(); + List detailIds = listDetails(settlementId).stream().map(PreSettlementDetail::getId).toList(); + List feeRows = detailIds.isEmpty() ? List.of() + : detailFeeMapper.selectList(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds) + .eq(PreSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(PreSettlementDetailFee::getCreateTime)); + for (PreSettlementDetailFee feeRow : feeRows) { + Map feeItems = parseFeeItems(feeRow.getFeeItemsJson()); + boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + if (!containsFreight) { + aggregates.merge(summaryKey("物流配送", "运输费"), money(feeRow.getFreightAmount()), + BigDecimal::add); + } + feeItems.forEach((feeItem, amount) -> aggregates.merge( + summaryKey(feeTypeMap.getOrDefault(feeItem, + isFreightFeeItem(feeItem) ? "物流配送" : "其他费用"), feeItem), + money(amount), BigDecimal::add)); + } + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId) + .eq(PreSettlementSummaryFee::getManualFlag, 0)); + int lineNo = 1; + for (Map.Entry entry : aggregates.entrySet()) { + String[] keyParts = entry.getKey().split("\\|", 2); + PreSettlementSummaryFee old = existingGenerated.get(entry.getKey()); + PreSettlementSummaryFee row = new PreSettlementSummaryFee(); + row.setPreSettlementId(settlementId); + row.setLineNo(lineNo++); + row.setFeeType(keyParts[0]); + row.setFeeItem(keyParts.length > 1 ? keyParts[1] : ""); + row.setOriginalAmount(money(entry.getValue())); + row.setAdjustAmount(preserveAdjustments && old != null ? money(old.getAdjustAmount()) + : BigDecimal.ZERO.setScale(2)); + row.setSettlementAmount(row.getOriginalAmount().add(row.getAdjustAmount())); + if (row.getSettlementAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("结算合计金额不能小于0"); + } + row.setRemark(old == null ? "" : old.getRemark()); + row.setManualFlag(0); + summaryFeeMapper.insert(row); + } + renumberSummaryFees(settlementId); + } + + private void applySummaryRequest(Long settlementId, List requestRows) { + if (requestRows == null) return; + Map> allowedManualFees = new LinkedHashMap<>(); + if (requestRows.stream().anyMatch(row -> Integer.valueOf(1).equals(row.getManualFlag()))) { + for (Map option : feeOptions()) { + Set feeItems = new LinkedHashSet<>(); + if (option.get("feeItems") instanceof List values) { + values.forEach(value -> feeItems.add(String.valueOf(value))); + } + allowedManualFees.put(String.valueOf(option.get("feeType")), feeItems); + } + } + Map existingMap = listSummaryFees(settlementId).stream() + .collect(Collectors.toMap(PreSettlementSummaryFee::getId, Function.identity())); + List manualRows = existingMap.values().stream() + .filter(row -> Integer.valueOf(1).equals(row.getManualFlag())).toList(); + Set retainedManualIds = new LinkedHashSet<>(); + for (PreSettlementSaveRequest.SummaryFee requestRow : requestRows) { + if (Integer.valueOf(1).equals(requestRow.getManualFlag())) { + String feeType = requiredText(requestRow.getFeeType(), "费用类型"); + String feeItem = requiredText(requestRow.getFeeItem(), "费用项"); + if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) { + throw new ServiceException("费用类型与费用项不匹配或费用项已停用"); + } + PreSettlementSummaryFee row = requestRow.getId() == null ? null : existingMap.get(requestRow.getId()); + if (row == null) { + row = existingMap.values().stream() + .filter(item -> Integer.valueOf(1).equals(item.getManualFlag())) + .filter(item -> Objects.equals(item.getFeeType(), requestRow.getFeeType()) + && Objects.equals(item.getFeeItem(), requestRow.getFeeItem())) + .findFirst().orElse(null); + } + if (row == null && requestRow.getId() == null) row = new PreSettlementSummaryFee(); + if (row == null || !Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的手工费用行"); + } + row.setPreSettlementId(settlementId); + row.setFeeType(feeType); + row.setFeeItem(feeItem); + row.setOriginalAmount(BigDecimal.ZERO.setScale(2)); + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + if (row.getAdjustAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("手工新增费用金额不能小于0"); + } + row.setSettlementAmount(row.getAdjustAmount()); + row.setRemark(limitRemark(requestRow.getRemark(), 50)); + row.setManualFlag(1); + if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row); + retainedManualIds.add(row.getId()); + saveChange(settlementId, "合计费用项", row.getLineNo(), requestRow.getId() == null ? "新增" : "调整", + row.getFeeItem() + "金额" + row.getSettlementAmount(), ""); + continue; + } + PreSettlementSummaryFee row = existingMap.get(requestRow.getId()); + if (row == null) { + row = existingMap.values().stream() + .filter(item -> !Integer.valueOf(1).equals(item.getManualFlag())) + .filter(item -> Objects.equals(item.getFeeType(), requestRow.getFeeType()) + && Objects.equals(item.getFeeItem(), requestRow.getFeeItem())) + .findFirst().orElse(null); + } + if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的结算合计行"); + } + BigDecimal before = money(row.getAdjustAmount()); + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + row.setSettlementAmount(money(row.getOriginalAmount()).add(row.getAdjustAmount())); + if (row.getSettlementAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("结算金额不能小于0"); + } + row.setRemark(limitRemark(requestRow.getRemark(), 50)); + summaryFeeMapper.updateById(row); + if (before.compareTo(row.getAdjustAmount()) != 0) { + saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", + "【调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】", ""); + } + } + for (PreSettlementSummaryFee manualRow : manualRows) { + if (!retainedManualIds.contains(manualRow.getId())) { + summaryFeeMapper.deleteById(manualRow.getId()); + saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除", + "删除" + manualRow.getFeeItem() + "费用" + manualRow.getSettlementAmount(), ""); + } + } + renumberSummaryFees(settlementId); + } + + private void refreshDetail(PreSettlementDetail detail) { + List fees = detailFees(detail.getId()); + BigDecimal freightAmount = fees.stream().map(PreSettlementDetailFee::getFreightAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal originalAmount = fees.stream().map(PreSettlementDetailFee::getOriginalAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal settlementAmountTax = fees.stream().map(PreSettlementDetailFee::getSettlementAmountTax) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + Map feeItems = new LinkedHashMap<>(); + fees.forEach(fee -> parseFeeItems(fee.getFeeItemsJson()).forEach((name, amount) -> + feeItems.merge(name, money(amount), BigDecimal::add))); + detail.setFreightAmount(money(freightAmount)); + detail.setOriginalAmount(money(originalAmount)); + detail.setAdjustAmount(money(settlementAmountTax.subtract(originalAmount))); + detail.setSettlementAmountTax(money(settlementAmountTax)); + detail.setFeeItemsJson(JsonUtil.toJson(feeItems)); + detailMapper.updateById(detail); + } + + private void refreshSettlementAmount(PreSettlement settlement) { + BigDecimal settlementAmount = listSummaryFees(settlement.getId()).stream() + .map(PreSettlementSummaryFee::getSettlementAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + settlement.setSettlementAmount(money(settlementAmount)); + BigDecimal rate = normalizeRate(settlement.getExchangeRate(), settlement.getCurrency()); + settlement.setExchangeRate(rate); + settlement.setLocalSettlementAmount(rate == null ? null : money(settlementAmount.multiply(rate))); + updateById(settlement); + } + + private void refreshAdvanceSummary(PreSettlement settlement) { + List advances = listAdvances(settlement.getId()).stream() + .filter(advance -> !STATUS_VOIDED.equals(advance.getBillStatus())).toList(); + settlement.setAdvanceNo(advances.stream().map(PreSettlementAdvance::getAdvanceNo) + .filter(Func::isNotEmpty).collect(Collectors.joining(","))); + settlement.setAdvanceAppliedAmount(advances.stream().map(PreSettlementAdvance::getAppliedAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + settlement.setAdvancePaidAmount(advances.stream().map(PreSettlementAdvance::getPaidAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + updateById(settlement); + } + + private void validateBeforeSubmit(PreSettlement settlement) { + if (listDetails(settlement.getId()).isEmpty()) { + throw new ServiceException("请至少选择一条结算明细"); + } + if (money(settlement.getSettlementAmount()).compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("结算金额必须大于0"); + } + if (!LOCAL_CURRENCY.equalsIgnoreCase(settlement.getCurrency())) { + if (settlement.getExchangeRateDate() == null) { + throw new ServiceException("外币结算必须选择汇率日期"); + } + if (settlement.getExchangeRate() == null || settlement.getExchangeRate().compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("外币结算必须填写大于0的结算汇率"); + } + } + } + + private PreSettlement loadExisting(Long id) { + PreSettlement settlement = id == null ? null : getById(id); + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) { + throw new ServiceException("预结算单不存在"); + } + return settlement; + } + + private PreSettlement loadEditable(Long id) { + PreSettlement settlement = loadExisting(id); + if (!List.of(STATUS_DRAFT, STATUS_RETURNED).contains(settlement.getApprovalStatus())) { + throw new ServiceException("仅草稿或已驳回的预结算单允许编辑"); + } + return settlement; + } + + private PreSettlement loadReviewing(Long id) { + PreSettlement settlement = loadExisting(id); + if (!STATUS_REVIEWING.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批中的预结算单允许执行该操作"); + } + return settlement; + } + + private ContractManage loadAvailableContract(Long contractId) { + ContractManage contract = contractManageService.getById(contractId); + if (contract == null || Objects.equals(contract.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(contract.getApprovalStatus()) + || "terminated".equals(contract.getContractStage()) + || !List.of("客户合同", "承运商合同").contains(contract.getContractCategory())) { + throw new ServiceException("仅允许选择审批完成且未作废的合同"); + } + validateContractRelations(contract); + return contract; + } + + private boolean isAvailableContract(ContractManage contract) { + if (contract == null || Objects.equals(contract.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(contract.getApprovalStatus()) + || "terminated".equals(contract.getContractStage()) || contract.getProjectId() == null + || !List.of("客户合同", "承运商合同").contains(contract.getContractCategory())) { + return false; + } + org.springblade.transport.pojo.entity.ProjectApply project = projectApplyService.getById(contract.getProjectId()); + if (project == null || Objects.equals(project.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) { + return false; + } + return isApprovedCustomer(contract.getPartyA()) && isApprovedCustomer(contract.getPartyB()); + } + + private Map contractFeeTypeMap(Long contractId) { + ContractManage contract = contractId == null ? null : contractManageService.getById(contractId); + Map result = new LinkedHashMap<>(); + if (contract == null) return result; + for (Map plan : parseList(contract.getBillingPlanJson())) { + if (!(plan.get("rules") instanceof List rules)) continue; + for (Object value : rules) { + if (!(value instanceof Map raw)) continue; + Object feeItem = raw.get("feeItem"); + Object feeType = raw.get("feeType"); + if (!isBlank(feeItem) && !isBlank(feeType)) { + result.putIfAbsent(String.valueOf(feeItem), String.valueOf(feeType)); + } + } + } + return result; + } + + private boolean isApprovedCustomer(String customerName) { + if (Func.isEmpty(customerName)) return false; + return customerArchiveService.count(Wrappers.lambdaQuery() + .eq(CustomerArchive::getIsDeleted, 0) + .eq(CustomerArchive::getStatus, 1) + .eq(CustomerArchive::getApprovalStatus, "approved") + .eq(CustomerArchive::getFullName, customerName)) > 0; + } + + private void validateContractRelations(ContractManage contract) { + if (contract.getProjectId() == null) { + throw new ServiceException("合同未关联已审批项目"); + } + org.springblade.transport.pojo.entity.ProjectApply project = projectApplyService.getById(contract.getProjectId()); + if (project == null || Objects.equals(project.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) { + throw new ServiceException("合同关联项目尚未完成审批"); + } + validateApprovedCustomer(contract.getPartyA()); + validateApprovedCustomer(contract.getPartyB()); + } + + private void validateApprovedCustomer(String customerName) { + if (!isApprovedCustomer(customerName)) { + throw new ServiceException("合同关联客商【" + customerName + "】尚未完成审批或已停用"); + } + } + + private void fillContract(PreSettlement settlement, ContractManage contract) { + settlement.setContractId(contract.getId()); + settlement.setContractNo(contract.getContractNo()); + settlement.setContractName(contract.getContractName()); + settlement.setProjectId(contract.getProjectId()); + settlement.setProjectName(contract.getProjectName()); + settlement.setDeptId(contract.getOrganizationId()); + settlement.setDeptName(contract.getOrganizationName()); + if ("receivable".equals(settlement.getSettlementType())) { + settlement.setPayerName(contract.getPartyB()); + settlement.setPayeeName(contract.getPartyA()); + } else { + settlement.setPayerName(contract.getPartyA()); + settlement.setPayeeName(contract.getPartyB()); + } + } + + private String resolveSettlementType(PreSettlementSaveRequest request, ContractManage contract) { + return contractSettlementType(contract); + } + + private String contractSettlementType(ContractManage contract) { + if ("客户合同".equals(contract.getContractCategory())) return "receivable"; + if ("承运商合同".equals(contract.getContractCategory())) return "payable"; + throw new ServiceException("合同类别不支持生成预结算单"); + } + + private void validateSettlementType(String settlementType) { + if (!List.of("receivable", "payable").contains(settlementType)) { + throw new ServiceException("结算类型不正确"); + } + } + + private void releaseSourceDetails(PreSettlement settlement, List details) { + for (PreSettlementDetail detail : details) { + ReceivablePayableDetail source = sourceDetailMapper.selectById(detail.getSourceDetailId()); + if (source != null && Objects.equals(source.getPreSettlementNo(), settlement.getPreSettlementNo()) + && Func.isEmpty(source.getFormalSettlementNo())) { + sourceDetailMapper.update(null, Wrappers.lambdaUpdate() + .set(ReceivablePayableDetail::getPreSettlementNo, null) + .set(ReceivablePayableDetail::getSettlementStatus, "pending") + .eq(ReceivablePayableDetail::getId, source.getId()) + .eq(ReceivablePayableDetail::getPreSettlementNo, settlement.getPreSettlementNo())); + } + } + } + + private void deleteChildren(Long settlementId) { + List detailIds = listDetails(settlementId).stream().map(PreSettlementDetail::getId).toList(); + if (!detailIds.isEmpty()) { + detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds)); + } + detailMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementDetail::getPreSettlementId, settlementId)); + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId)); + advanceMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, settlementId)); + changeRecordMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementChangeRecord::getPreSettlementId, settlementId)); + } + + private List listDetails(Long settlementId) { + return detailMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementDetail::getPreSettlementId, settlementId) + .eq(PreSettlementDetail::getIsDeleted, 0) + .orderByAsc(PreSettlementDetail::getLineNo)); + } + + private List listSummaryFees(Long settlementId) { + return summaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId) + .eq(PreSettlementSummaryFee::getIsDeleted, 0) + .orderByAsc(PreSettlementSummaryFee::getLineNo)); + } + + private List listAdvances(Long settlementId) { + return advanceMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, settlementId) + .eq(PreSettlementAdvance::getIsDeleted, 0) + .orderByDesc(PreSettlementAdvance::getCreateTime)); + } + + private void renumberDetails(Long settlementId) { + List details = listDetails(settlementId); + for (int index = 0; index < details.size(); index++) { + PreSettlementDetail detail = details.get(index); + detail.setLineNo(index + 1); + detailMapper.updateById(detail); + } + } + + private void renumberSummaryFees(Long settlementId) { + List rows = listSummaryFees(settlementId); + for (int index = 0; index < rows.size(); index++) { + PreSettlementSummaryFee row = rows.get(index); + row.setLineNo(index + 1); + summaryFeeMapper.updateById(row); + } + } + + private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, + String content, String reason) { + PreSettlementChangeRecord record = new PreSettlementChangeRecord(); + record.setPreSettlementId(settlementId); + record.setChangeType(changeType); + record.setLineNo(lineNo); + record.setOperationType(operationType); + record.setChangeContent(content); + record.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName()); + record.setChangeReason(reason); + record.setChangeTime(LocalDateTime.now()); + changeRecordMapper.insert(record); + } + + private List> resolvePrintTemplates(PreSettlement settlement) { + ContractManage contract = contractManageService.getById(settlement.getContractId()); + List> templates = new ArrayList<>(); + if (contract != null && Func.isNotEmpty(contract.getPreSettlementConfigJson())) { + try { + Object parsed = JsonUtil.parse(contract.getPreSettlementConfigJson(), Map.class); + if (parsed instanceof Map config && config.get("printTemplates") instanceof List rows) { + for (Object value : rows) { + if (value instanceof Map raw) { + Object nameValue = raw.get("name"); + String name = nameValue == null ? "" : String.valueOf(nameValue); + if (!name.isBlank()) { + templates.add(Map.of("value", name, "label", name)); + } + } + } + } + } catch (Exception ignored) { + // 兼容历史配置,使用系统默认打印模板。 + } + } + if (templates.isEmpty()) { + templates.add(Map.of("value", "default", "label", "默认模板(结算基本信息+结算合计)")); + } + return templates; + } + + private Map parseFeeItems(String json) { + Map result = new LinkedHashMap<>(); + if (Func.isEmpty(json)) return result; + try { + Object parsed = JsonUtil.parse(json, Map.class); + if (parsed instanceof Map map) { + map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value))); + } + } catch (Exception ignored) { + // 历史脏数据不影响结算单展示。 + } + return result; + } + + private Map normalizeFeeItems(Map feeItems) { + Map result = new LinkedHashMap<>(); + if (feeItems == null) return result; + feeItems.forEach((key, value) -> result.put(key, nonNegative(value, key))); + return result; + } + + private BigDecimal calculateFeeAmount(PreSettlementDetailFee fee) { + Map feeItems = parseFeeItems(fee.getFeeItemsJson()); + BigDecimal feeItemTotal = feeItems.values().stream().map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + return money(containsFreight ? feeItemTotal : money(fee.getFreightAmount()).add(feeItemTotal)); + } + + private boolean isFreightFeeItem(String name) { + return name != null && (name.contains("运费") || name.contains("运输费")); + } + + private BigDecimal nonNegative(BigDecimal value, String fieldName) { + BigDecimal normalized = value == null ? BigDecimal.ZERO : value; + if (normalized.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException(fieldName + "不能小于0"); + } + return normalized; + } + + private BigDecimal normalizeRate(BigDecimal exchangeRate, String currency) { + if (LOCAL_CURRENCY.equalsIgnoreCase(Func.isEmpty(currency) ? LOCAL_CURRENCY : currency)) { + return BigDecimal.ONE.setScale(2, RoundingMode.HALF_UP); + } + if (exchangeRate == null) return null; + if (exchangeRate.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("结算汇率必须大于0"); + } + return exchangeRate.setScale(6, RoundingMode.HALF_UP); + } + + private BigDecimal money(BigDecimal value) { + return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal decimal(Object value) { + if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO; + try { + return new BigDecimal(String.valueOf(value)); + } catch (NumberFormatException exception) { + return BigDecimal.ZERO; + } + } + + @SuppressWarnings("unchecked") + private List> parseList(String json) { + if (Func.isEmpty(json)) return List.of(); + try { + Object parsed = JsonUtil.parse(json, List.class); + if (parsed instanceof List list) { + return list.stream().filter(Map.class::isInstance).map(item -> { + Map result = new LinkedHashMap<>(); + ((Map) item).forEach((key, value) -> result.put(String.valueOf(key), value)); + return result; + }).toList(); + } + } catch (Exception ignored) { + // 历史脏数据不影响费用类型映射。 + } + return List.of(); + } + + private boolean isBlank(Object value) { + return value == null || String.valueOf(value).isBlank(); + } + + private LocalDate parseDate(String value) { + try { + return Func.isEmpty(value) ? null : LocalDate.parse(value); + } catch (Exception exception) { + throw new ServiceException("日期格式应为yyyy-MM-dd"); + } + } + + private String requiredText(String value, String fieldName) { + if (value == null || value.trim().isEmpty()) { + throw new ServiceException(fieldName + "不能为空"); + } + return value.trim(); + } + + private String limitRemark(String value, int maxLength) { + if (value != null && value.length() > maxLength) { + throw new ServiceException("备注不能超过" + maxLength + "个字符"); + } + return value; + } + + private String limitText(String value, int maxLength, String fieldName) { + if (value != null && value.length() > maxLength) { + throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符"); + } + return value; + } + + private void appendChange(List changes, String fieldName, BigDecimal before, BigDecimal after) { + BigDecimal oldValue = money(before); + BigDecimal newValue = money(after); + if (oldValue.compareTo(newValue) != 0) { + changes.add("【" + fieldName + "】从【" + oldValue + "】调整为【" + newValue + "】"); + } + } + + private void appendTextChange(List changes, String fieldName, String before, String after) { + String oldValue = before == null ? "" : before; + String newValue = after == null ? "" : after; + if (!Objects.equals(oldValue, newValue)) { + changes.add("【" + fieldName + "】从【" + oldValue + "】调整为【" + newValue + "】"); + } + } + + private String firstNotEmpty(String first, String second) { + return Func.isNotEmpty(first) ? first : second; + } + + private String summaryKey(PreSettlementSummaryFee row) { + return summaryKey(row.getFeeType(), row.getFeeItem()); + } + + private String summaryKey(String feeType, String feeItem) { + return String.valueOf(feeType) + "|" + String.valueOf(feeItem); + } + + private synchronized String nextPreSettlementNo() { + return nextDailyCode("YJ", PreSettlement::getPreSettlementNo); + } + + private synchronized String nextFormalSettlementNo() { + return nextDailyCode("ZJ", PreSettlement::getFormalSettlementNo); + } + + private synchronized String nextAdvanceNo() { + String prefix = "YF" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + PreSettlementAdvance latest = advanceMapper.selectOne(Wrappers.lambdaQuery() + .likeRight(PreSettlementAdvance::getAdvanceNo, prefix) + .orderByDesc(PreSettlementAdvance::getAdvanceNo) + .last("limit 1")); + return prefix + String.format("%05d", nextSequence(latest == null ? null : latest.getAdvanceNo(), prefix)); + } + + private String nextDailyCode(String code, Function getter) { + String prefix = code + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + PreSettlement latest = list(Wrappers.lambdaQuery() + .and(query -> query.likeRight(PreSettlement::getPreSettlementNo, prefix) + .or().likeRight(PreSettlement::getFormalSettlementNo, prefix)) + .orderByDesc(PreSettlement::getCreateTime) + .last("limit 1")).stream().max(Comparator.comparing(item -> { + String value = getter.apply(item); + return value == null ? "" : value; + })).orElse(null); + String latestCode = latest == null ? null : getter.apply(latest); + return prefix + String.format("%05d", nextSequence(latestCode, prefix)); + } + + private int nextSequence(String latestCode, String prefix) { + if (latestCode == null || !latestCode.startsWith(prefix)) return 1; + try { + return Integer.parseInt(latestCode.substring(prefix.length())) + 1; + } catch (NumberFormatException exception) { + return 1; + } + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java new file mode 100644 index 0000000..ed750e1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java @@ -0,0 +1,45 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.vo.FormalSettlementVO; + +import java.util.Objects; + +/** + * 正式结算单包装类 + * + * @author Chill + */ +public class FormalSettlementWrapper extends BaseEntityWrapper { + + public static FormalSettlementWrapper build() { + return new FormalSettlementWrapper(); + } + + @Override + public FormalSettlementVO entityVO(FormalSettlement entity) { + FormalSettlementVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, FormalSettlementVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付"); + vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> entity.getApprovalStatus(); + }); + return vo; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java new file mode 100644 index 0000000..6832c40 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java @@ -0,0 +1,51 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.vo.PreSettlementVO; + +import java.util.Objects; + +/** + * 预结算单包装类 + * + * @author Chill + */ +public class PreSettlementWrapper extends BaseEntityWrapper { + + public static PreSettlementWrapper build() { + return new PreSettlementWrapper(); + } + + @Override + public PreSettlementVO entityVO(PreSettlement entity) { + PreSettlementVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, PreSettlementVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setApprovalStatusName(approvalStatusName(entity.getApprovalStatus())); + vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付"); + return vo; + } + + private String approvalStatusName(String status) { + return switch (status == null ? "" : status) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> status; + }; + } + +} diff --git a/doc/sql/transport/blade_formal_settlement_20260818.sql b/doc/sql/transport/blade_formal_settlement_20260818.sql new file mode 100644 index 0000000..bc05c11 --- /dev/null +++ b/doc/sql/transport/blade_formal_settlement_20260818.sql @@ -0,0 +1,90 @@ +-- 结算管理 / 正式结算单 + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement` ( + `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `formal_settlement_no` varchar(100) NOT NULL COMMENT '正式结算单号', `source_type` varchar(30) NOT NULL DEFAULT '预结算合并', + `settlement_type` varchar(30) NOT NULL, `project_id` bigint(20) DEFAULT NULL, `project_name` varchar(100) DEFAULT NULL, + `dept_id` bigint(20) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `contract_id` bigint(20) NOT NULL, + `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(100) NOT NULL, `payer_name` varchar(200) DEFAULT NULL, + `payee_name` varchar(200) DEFAULT NULL, `currency` varchar(20) NOT NULL DEFAULT 'RMB', `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `local_currency` varchar(20) NOT NULL DEFAULT 'RMB', `local_settlement_amount` decimal(18,2) DEFAULT NULL, + `applied_payment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请付款金额含预付', + `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已收已付合计', + `exchange_rate_date` date DEFAULT NULL, `exchange_rate` decimal(18,6) DEFAULT NULL, + `invoice_status` varchar(30) NOT NULL DEFAULT 'unreceived', `payment_status` varchar(30) NOT NULL DEFAULT 'unpaid', + `approval_status` varchar(30) NOT NULL DEFAULT 'draft', `current_node` varchar(100) DEFAULT NULL, `current_processor` varchar(200) DEFAULT NULL, + `kingdee_bill_no` varchar(100) DEFAULT NULL, `kingdee_sync_status` varchar(30) NOT NULL DEFAULT 'unsynced', + `attachments_json` longtext, `remark` varchar(200) DEFAULT NULL, `approved_time` datetime DEFAULT NULL, `synced_time` datetime DEFAULT NULL, + `void_reason` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_formal_settlement_no` (`tenant_id`,`formal_settlement_no`), + KEY `idx_formal_contract` (`contract_id`), KEY `idx_formal_approval` (`approval_status`), KEY `idx_formal_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算单'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_source` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `formal_settlement_id` bigint(20) NOT NULL, `pre_settlement_id` bigint(20) NOT NULL, `pre_settlement_no` varchar(100) NOT NULL, + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `advance_applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `advance_paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00', PRIMARY KEY (`id`), + KEY `idx_formal_source_pre` (`pre_settlement_id`), KEY `idx_formal_source_bill` (`formal_settlement_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算来源预结算'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_detail` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `formal_settlement_id` bigint(20) NOT NULL, `source_pre_settlement_id` bigint(20) DEFAULT NULL, + `source_pre_settlement_detail_id` bigint(20) DEFAULT NULL, `source_detail_id` bigint(20) NOT NULL, `line_no` int(11) NOT NULL, + `document_no` varchar(100) DEFAULT NULL, `waybill_id` bigint(20) DEFAULT NULL, `waybill_no` varchar(100) DEFAULT NULL, + `vehicle_no` varchar(100) DEFAULT NULL, `departure_address` varchar(500) DEFAULT NULL, `arrival_address` varchar(500) DEFAULT NULL, + `actual_departure_time` datetime DEFAULT NULL, `actual_completion_time` datetime DEFAULT NULL, `transport_type` varchar(100) DEFAULT NULL, + `cargo_name` varchar(500) DEFAULT NULL, `cargo_type` varchar(500) DEFAULT NULL, `transport_quantity` decimal(18,6) DEFAULT NULL, + `quantity_unit` varchar(50) DEFAULT NULL, `mileage` decimal(18,2) DEFAULT NULL, `batch_no` varchar(100) DEFAULT NULL, + `unit_price` decimal(18,2) DEFAULT NULL, `freight_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `fee_items_json` longtext, + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00', `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL, + `currency` varchar(20) NOT NULL DEFAULT 'RMB', `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), + KEY `idx_formal_detail_bill` (`formal_settlement_id`), KEY `idx_formal_detail_source` (`source_detail_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算明细快照'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_detail_fee` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `formal_settlement_detail_id` bigint(20) NOT NULL, `source_fee_id` bigint(20) DEFAULT NULL, `line_no` varchar(30) DEFAULT NULL, + `cargo_name` varchar(100) DEFAULT NULL, `cargo_type` varchar(100) DEFAULT NULL, `transport_quantity` decimal(18,6) DEFAULT NULL, + `quantity_unit` varchar(50) DEFAULT NULL, `mileage` decimal(18,2) DEFAULT NULL, `unit_price` decimal(18,2) DEFAULT NULL, + `freight_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `fee_items_json` longtext, `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00', + `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), + KEY `idx_formal_detail_fee_detail` (`formal_settlement_detail_id`), KEY `idx_formal_detail_fee_source` (`source_fee_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算货物费用快照'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_payment` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `formal_settlement_id` bigint(20) NOT NULL, `payment_no` varchar(100) NOT NULL, `payment_type` varchar(30) NOT NULL DEFAULT 'final', + `applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `bill_status` varchar(30) NOT NULL DEFAULT 'reviewing', `kingdee_bill_no` varchar(100) DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), UNIQUE KEY `uk_formal_payment_no` (`tenant_id`,`payment_no`), + KEY `idx_formal_payment_bill` (`formal_settlement_id`), KEY `idx_formal_payment_status` (`bill_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算付款申请'; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000001040,2090000000000001000,'formal_settlement','正式结算单','formal_settlement','/settlement/formal-settlement','iconfont icon-caidanguanli',4,1,0,1,NULL,'',0), +(2090000000000001041,2090000000000001040,'formal_settlement_view','查看','formal_settlement_view','','',1,2,0,1,NULL,'',0), +(2090000000000001042,2090000000000001040,'formal_settlement_add','新增','formal_settlement_add','','',2,2,0,1,NULL,'',0), +(2090000000000001043,2090000000000001040,'formal_settlement_edit','编辑','formal_settlement_edit','','',3,2,0,1,NULL,'',0), +(2090000000000001044,2090000000000001040,'formal_settlement_delete','删除','formal_settlement_delete','','',4,2,0,1,NULL,'',0), +(2090000000000001045,2090000000000001040,'formal_settlement_submit','提交审批','formal_settlement_submit','','',5,2,0,1,NULL,'',0), +(2090000000000001046,2090000000000001040,'formal_settlement_approve','审批','formal_settlement_approve','','',6,2,0,1,NULL,'',0), +(2090000000000001047,2090000000000001040,'formal_settlement_sync','同步金蝶','formal_settlement_sync','','',7,2,0,1,NULL,'',0), +(2090000000000001048,2090000000000001040,'formal_settlement_print','打印','formal_settlement_print','','',8,2,0,1,NULL,'',0), +(2090000000000001049,2090000000000001040,'formal_settlement_export','导出','formal_settlement_export','','',9,2,0,1,NULL,'',0), +(2090000000000001050,2090000000000001040,'formal_settlement_void','作废','formal_settlement_void','','',10,2,0,1,NULL,'',0), +(2090000000000001051,2090000000000001040,'formal_settlement_adjust','明细调整','formal_settlement_adjust','','',11,2,0,1,NULL,'',0), +(2090000000000001052,2090000000000001040,'formal_settlement_payment','付款申请','formal_settlement_payment','','',12,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_pre_settlement_20260818.sql b/doc/sql/transport/blade_pre_settlement_20260818.sql new file mode 100644 index 0000000..cc3b761 --- /dev/null +++ b/doc/sql/transport/blade_pre_settlement_20260818.sql @@ -0,0 +1,214 @@ +-- 结算管理 / 预结算 + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '预结算单号', + `source_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '应收应付' COMMENT '来源', + `settlement_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'payable' COMMENT '结算类型', + `project_id` bigint(20) DEFAULT NULL COMMENT '项目ID', + `project_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目名称', + `dept_id` bigint(20) DEFAULT NULL COMMENT '所属组织ID', + `dept_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所属组织', + `contract_id` bigint(20) NOT NULL COMMENT '合同ID', + `contract_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同编号', + `contract_name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同名称', + `payer_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '付款方', + `payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收款方', + `currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '结算币种', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `local_currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '本位币', + `local_settlement_amount` decimal(18,2) DEFAULT NULL COMMENT '本位币合计', + `exchange_rate_date` date DEFAULT NULL COMMENT '汇率日期', + `exchange_rate` decimal(18,6) DEFAULT NULL COMMENT '结算汇率', + `approval_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'draft' COMMENT '审核状态', + `current_node` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前节点', + `current_processor` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前处理人', + `advance_no` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '预付单号', + `advance_applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请预付金额', + `advance_paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已付款金额', + `formal_settlement_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '正式结算单号', + `attachments_json` longtext COLLATE utf8mb4_general_ci COMMENT '附件JSON', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `approved_time` datetime DEFAULT NULL COMMENT '审核通过时间', + `formal_settled_time` datetime DEFAULT NULL COMMENT '正式结算时间', + `void_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '作废原因', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_pre_settlement_no` (`tenant_id`,`pre_settlement_no`) USING BTREE, + KEY `idx_pre_settlement_contract` (`contract_id`) USING BTREE, + KEY `idx_pre_settlement_project` (`project_id`) USING BTREE, + KEY `idx_pre_settlement_approval` (`approval_status`) USING BTREE, + KEY `idx_pre_settlement_create_time` (`create_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算单'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_detail` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_id` bigint(20) NOT NULL COMMENT '预结算单ID', + `source_detail_id` bigint(20) NOT NULL COMMENT '应收应付明细ID', + `line_no` int(11) NOT NULL COMMENT '行号', + `document_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '单据号', + `waybill_id` bigint(20) DEFAULT NULL COMMENT '运单ID', + `waybill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '运单号', + `vehicle_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '车号', + `departure_address` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货地址', + `arrival_address` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '到货地址', + `actual_departure_time` datetime DEFAULT NULL COMMENT '实际发货时间', + `actual_completion_time` datetime DEFAULT NULL COMMENT '实际完成时间', + `transport_type` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '运输类型', + `cargo_name` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物名称', + `cargo_type` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物类型', + `transport_quantity` decimal(18,6) DEFAULT NULL COMMENT '运输总量', + `quantity_unit` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数量单位', + `mileage` decimal(18,2) DEFAULT NULL COMMENT '里程', + `batch_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '批次号', + `unit_price` decimal(18,2) DEFAULT NULL COMMENT '运输单价', + `freight_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '运费', + `fee_items_json` longtext COLLATE utf8mb4_general_ci COMMENT '费用项JSON', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额(含税)', + `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL COMMENT '结算金额(不含税)', + `currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '币种', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_source_detail` (`source_detail_id`) USING BTREE, + KEY `idx_pre_settlement_detail_bill` (`pre_settlement_id`) USING BTREE, + KEY `idx_pre_settlement_detail_waybill` (`waybill_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算明细'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_detail_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_detail_id` bigint(20) NOT NULL COMMENT '预结算明细ID', + `source_fee_id` bigint(20) DEFAULT NULL COMMENT '源费用行ID', + `line_no` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '行号', + `cargo_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物名称', + `cargo_type` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物类型', + `transport_quantity` decimal(18,6) DEFAULT NULL COMMENT '运输量', + `quantity_unit` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数量单位', + `mileage` decimal(18,2) DEFAULT NULL COMMENT '里程', + `unit_price` decimal(18,2) DEFAULT NULL COMMENT '运输单价', + `freight_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '运费', + `fee_items_json` longtext COLLATE utf8mb4_general_ci COMMENT '费用项JSON', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额(含税)', + `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL COMMENT '结算金额(不含税)', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_detail_fee_detail` (`pre_settlement_detail_id`) USING BTREE, + KEY `idx_pre_settlement_detail_fee_source` (`source_fee_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算明细费用快照'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_summary_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_id` bigint(20) NOT NULL COMMENT '预结算单ID', + `line_no` int(11) NOT NULL COMMENT '行号', + `fee_type` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型', + `fee_item` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `remark` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `manual_flag` int(11) NOT NULL DEFAULT '0' COMMENT '是否手工添加', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_summary_bill` (`pre_settlement_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算合计费用'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_advance` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_id` bigint(20) NOT NULL COMMENT '预结算单ID', + `advance_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '预付单号', + `applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请预付金额', + `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已付款金额', + `bill_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'reviewing' COMMENT '单据状态', + `kingdee_advance_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶预付单号', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_pre_settlement_advance_no` (`tenant_id`,`advance_no`) USING BTREE, + KEY `idx_pre_settlement_advance_bill` (`pre_settlement_id`) USING BTREE, + KEY `idx_pre_settlement_advance_status` (`bill_status`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算预付记录'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_change_record` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_id` bigint(20) NOT NULL COMMENT '预结算单ID', + `change_type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '变更类型', + `line_no` int(11) DEFAULT NULL COMMENT '行号', + `operation_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL COMMENT '操作类型', + `change_content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT '变更内容', + `operator_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作人', + `change_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更原因', + `change_time` datetime NOT NULL COMMENT '变更时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_change_bill` (`pre_settlement_id`) USING BTREE, + KEY `idx_pre_settlement_change_time` (`change_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算变更记录'; + +-- 结算管理菜单下新增预结算及按钮权限。 +INSERT INTO `blade_menu` +(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) +VALUES +(2090000000000001020, 2090000000000001000, 'pre_settlement', '预结算', 'pre_settlement', '/settlement/pre-settlement', 'iconfont icon-caidanguanli', 3, 1, 0, 1, NULL, '', 0), +(2090000000000001021, 2090000000000001020, 'pre_settlement_view', '查看', 'pre_settlement_view', '', '', 1, 2, 0, 1, NULL, '', 0), +(2090000000000001022, 2090000000000001020, 'pre_settlement_add', '新增', 'pre_settlement_add', '', '', 2, 2, 0, 1, NULL, '', 0), +(2090000000000001023, 2090000000000001020, 'pre_settlement_edit', '编辑', 'pre_settlement_edit', '', '', 3, 2, 0, 1, NULL, '', 0), +(2090000000000001024, 2090000000000001020, 'pre_settlement_delete', '删除', 'pre_settlement_delete', '', '', 4, 2, 0, 1, NULL, '', 0), +(2090000000000001025, 2090000000000001020, 'pre_settlement_submit', '提交审批', 'pre_settlement_submit', '', '', 5, 2, 0, 1, NULL, '', 0), +(2090000000000001026, 2090000000000001020, 'pre_settlement_approve', '审批', 'pre_settlement_approve', '', '', 6, 2, 0, 1, NULL, '', 0), +(2090000000000001027, 2090000000000001020, 'pre_settlement_advance', '预付申请', 'pre_settlement_advance', '', '', 7, 2, 0, 1, NULL, '', 0), +(2090000000000001028, 2090000000000001020, 'pre_settlement_formal', '尾款结算', 'pre_settlement_formal', '', '', 8, 2, 0, 1, NULL, '', 0), +(2090000000000001029, 2090000000000001020, 'pre_settlement_print', '打印结算单', 'pre_settlement_print', '', '', 9, 2, 0, 1, NULL, '', 0), +(2090000000000001030, 2090000000000001020, 'pre_settlement_export', '导出', 'pre_settlement_export', '', '', 10, 2, 0, 1, NULL, '', 0), +(2090000000000001031, 2090000000000001020, 'pre_settlement_adjust', '明细调整', 'pre_settlement_adjust', '', '', 11, 2, 0, 1, NULL, '', 0), +(2090000000000001032, 2090000000000001020, 'pre_settlement_void', '作废', 'pre_settlement_void', '', '', 12, 2, 0, 1, NULL, '', 0) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `path` = VALUES(`path`), + `sort` = VALUES(`sort`), + `is_deleted` = 0; From 83fb67a142115df4413ca64127a844531e8e4561 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 21:48:39 +0800 Subject: [PATCH 026/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BA=94=E6=94=B6?= =?UTF-8?q?=E5=BA=94=E4=BB=98=E6=98=8E=E7=BB=86=E5=8D=95=E4=BB=B7=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=B8=8D=E5=AF=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReceivablePayableDetailServiceImpl.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index d0e8be8..6a06d17 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -357,11 +357,13 @@ public class ReceivablePayableDetailServiceImpl try { String planId = matchedPlanId(waybill, contract); if (Func.isEmpty(planId)) continue; + // 自动生成只按合同计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 List matchedFees = calculatedFees(waybill, contract, planId, true); if (matchedFees.isEmpty()) continue; + BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); for (String settlementType : List.of("payable", "receivable")) { if (existsByWaybill(waybill.getId(), settlementType)) continue; - ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees); + ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees, contractUnitPrice); save(detail); for (ReceivablePayableCargoFee fee : matchedFees) { ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); @@ -510,6 +512,11 @@ public class ReceivablePayableDetailServiceImpl } private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String settlementType, List fees) { + return buildDetail(waybill, contract, settlementType, fees, resolveContractUnitPrice(fees)); + } + + private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String settlementType, + List fees, BigDecimal unitPrice) { BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP); @@ -538,7 +545,7 @@ public class ReceivablePayableDetailServiceImpl detail.setQuantityUnit(waybill.getQuantityUnit()); detail.setMileage(waybill.getMileage()); detail.setBatchNo(waybill.getBatchNo()); - detail.setUnitPrice(waybill.getUnitPrice()); + detail.setUnitPrice(unitPrice); detail.setCurrency("RMB"); detail.setFreightAmount(freight); detail.setOtherFeeAmount(other); @@ -549,6 +556,19 @@ public class ReceivablePayableDetailServiceImpl return detail; } + private BigDecimal resolveContractUnitPrice(List fees) { + return fees.stream() + .filter(this::isFreight) + .map(ReceivablePayableCargoFee::getUnitPrice) + .filter(Objects::nonNull) + .findFirst() + .orElseGet(() -> fees.stream() + .map(ReceivablePayableCargoFee::getUnitPrice) + .filter(Objects::nonNull) + .findFirst() + .orElse(BigDecimal.ZERO)); + } + private ReceivablePayableCargoFee buildCargoFee(Long detailId, Waybill waybill) { BigDecimal quantity = money(waybill.getQuantity()); BigDecimal unitPrice = money(waybill.getUnitPrice()); @@ -807,6 +827,7 @@ public class ReceivablePayableDetailServiceImpl detail.setFreightAmount(freight); detail.setOtherFeeAmount(total.subtract(freight)); detail.setTotalAmount(total); + detail.setUnitPrice(resolveContractUnitPrice(fees)); detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); updateById(detail); } From e7ca06736c36e57fee67f2fe17773ce64ad1eb36 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 22:10:24 +0800 Subject: [PATCH 027/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8DIAM401=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/springblade/auth/endpoint/IamSsoEndpoint.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/IamSsoEndpoint.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/IamSsoEndpoint.java index a428dd6..ed27b2e 100644 --- a/blade-auth/src/main/java/org/springblade/auth/endpoint/IamSsoEndpoint.java +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/IamSsoEndpoint.java @@ -27,6 +27,7 @@ package org.springblade.auth.endpoint; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint; @@ -60,6 +61,7 @@ public class IamSsoEndpoint { * @return token */ @RequestMapping(value = "/token", method = {RequestMethod.GET, RequestMethod.POST}) + @PermitAll @Operation(summary = "IAM统一身份认证登录", description = "使用IAM授权码换取本系统Token") public ResponseEntity token(HttpServletRequest request) { String grantType = request.getParameter("grant_type"); From 8bb18c6f6af60c1d0bb09be4ee68525f40c3674b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 22:33:49 +0800 Subject: [PATCH 028/114] =?UTF-8?q?=E4=BF=AE=E5=A4=8DIAM=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/IamSsoProfileResponse.java | 26 +++++++++++++++---- .../auth/granter/IamSsoTokenGranter.java | 17 +++++++++--- .../handler/BladeAuthorizationHandler.java | 5 ++++ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/blade-auth/src/main/java/org/springblade/auth/dto/IamSsoProfileResponse.java b/blade-auth/src/main/java/org/springblade/auth/dto/IamSsoProfileResponse.java index d51f7f8..c2ff4f6 100644 --- a/blade-auth/src/main/java/org/springblade/auth/dto/IamSsoProfileResponse.java +++ b/blade-auth/src/main/java/org/springblade/auth/dto/IamSsoProfileResponse.java @@ -26,6 +26,7 @@ package org.springblade.auth.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Data; import java.util.Map; @@ -43,6 +44,13 @@ public class IamSsoProfileResponse { */ private String id; + /** + * IAM返回的本系统账号,部分部署直接返回在顶层。 + */ + @JsonProperty("account_no") + @JsonAlias({"accountNo", "account", "username", "user_name"}) + private String accountNo; + /** * IAM用户扩展属性 */ @@ -54,12 +62,20 @@ public class IamSsoProfileResponse { * @return 本系统账号 */ @JsonProperty(access = JsonProperty.Access.READ_ONLY) - public String getAccountNo() { - if (attributes == null) { - return null; + public String resolveAccountNo() { + if (accountNo != null && !accountNo.isBlank()) { + return accountNo; } - Object accountNo = attributes.get("account_no"); - return accountNo == null ? null : String.valueOf(accountNo); + if (attributes == null) { + return id; + } + for (String key : new String[]{"account_no", "accountNo", "account", "username", "user_name"}) { + Object value = attributes.get(key); + if (value != null && !String.valueOf(value).isBlank()) { + return String.valueOf(value); + } + } + return id; } } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index 6c6d279..c1dfecf 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -143,7 +143,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { } IamSsoProfileResponse profileResponse = requestIamProfile(accessToken); - String accountNo = profileResponse.getAccountNo(); + String accountNo = profileResponse.resolveAccountNo(); if (StringUtil.isBlank(accountNo)) { log.warn("IAM统一身份认证用户信息缺少account_no,iamId={}", profileResponse.getId()); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); @@ -165,23 +165,32 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { private UserInfo loadOrCreateIamUser(OAuth2Request request, IamSsoProfileResponse profileResponse, String tenantId, String accountNo) { R result = userClient.userInfo(tenantId, accountNo); - if (result.isSuccess() && result.getData() != null) { + if (result.isSuccess() && hasUser(result.getData())) { return result.getData(); } - log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}", tenantId, accountNo); + log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}", + tenantId, accountNo, result.isSuccess()); R saveResult = userClient.saveIamUser(buildIamUser(profileResponse, tenantId, accountNo)); if (!saveResult.isSuccess() || !Boolean.TRUE.equals(saveResult.getData())) { log.warn("IAM统一身份认证自动创建用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, saveResult.getMsg()); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); } R createdResult = userClient.userInfo(tenantId, accountNo); - if (!createdResult.isSuccess() || createdResult.getData() == null) { + if (!createdResult.isSuccess() || !hasUser(createdResult.getData())) { log.warn("IAM统一身份认证自动创建用户后未查询到用户,tenantId={}, accountNo={}", tenantId, accountNo); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); } return createdResult.getData(); } + /** + * Feign 返回成功时,data 仍可能是一个 user 为空的 UserInfo 包装对象。 + * IAM 登录必须确保本地用户实体存在,避免被误报为密码校验失败。 + */ + private boolean hasUser(UserInfo userInfo) { + return userInfo != null && userInfo.getUser() != null; + } + private User buildIamUser(IamSsoProfileResponse profileResponse, String tenantId, String accountNo) { User user = new User(); user.setTenantId(tenantId); diff --git a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java index 1526d27..e6e793e 100644 --- a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java +++ b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java @@ -40,6 +40,7 @@ import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DesUtil; import org.springblade.core.tool.utils.SM2Util; +import org.springblade.core.tool.utils.StringUtil; import org.springblade.system.cache.SysCache; import org.springblade.system.pojo.entity.Tenant; @@ -74,6 +75,10 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler { */ @Override public OAuth2Validation preValidation(OAuth2Request request) { + // IAM授权码已经由外部身份系统完成认证,不读取或校验本地密码。 + if (StringUtil.equals("iam_sso", request.getGrantType())) { + return new OAuth2Validation(); + } if (request.isPassword() || request.isCaptchaCode()) { // 生产环境弱密码校验 if (bladeProperties.isProd() && isWeakPassword(request.getPassword())) { From df6d417e40d223fe9233687227072cd184b08c5f Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 19 Aug 2026 06:53:39 +0800 Subject: [PATCH 029/114] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E8=B0=83=E6=95=B4=E5=8D=95=202=E3=80=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=BF=90=E8=BE=93=E5=AF=B9=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/ApiScopePermissionHandler.java | 4 + .../pojo/dto/PreSettlementSaveRequest.java | 6 + .../dto/SettlementAdjustmentSaveRequest.java | 28 + .../SettlementAdjustmentStatusRequest.java | 12 + ...sportReconciliationManualMatchRequest.java | 15 + .../TransportReconciliationSaveRequest.java | 18 + .../pojo/entity/SettlementAdjustment.java | 35 ++ .../entity/SettlementAdjustmentDetail.java | 25 + .../pojo/entity/TransportReconciliation.java | 68 +++ .../TransportReconciliationChangeRecord.java | 39 ++ .../TransportReconciliationExternal.java | 49 ++ .../TransportReconciliationInternal.java | 55 ++ .../pojo/vo/SettlementAdjustmentVO.java | 22 + .../pojo/vo/TransportReconciliationVO.java | 28 + .../service/ISettlementAdjustmentService.java | 23 + .../ReceivablePayableDetailController.java | 9 +- .../SettlementAdjustmentController.java | 36 ++ .../TransportReconciliationController.java | 144 +++++ .../excel/CargoReconciliationExcel.java | 41 ++ .../CargoReconciliationFailureExcel.java | 13 + .../excel/VehicleReconciliationExcel.java | 40 ++ .../VehicleReconciliationFailureExcel.java | 13 + .../SettlementAdjustmentDetailMapper.java | 8 + .../mapper/SettlementAdjustmentMapper.java | 8 + ...sportReconciliationChangeRecordMapper.java | 11 + ...TransportReconciliationExternalMapper.java | 11 + ...TransportReconciliationInternalMapper.java | 11 + .../mapper/TransportReconciliationMapper.java | 11 + .../IReceivablePayableDetailService.java | 3 +- .../ITransportReconciliationService.java | 39 ++ .../impl/PreSettlementServiceImpl.java | 19 +- .../ReceivablePayableDetailServiceImpl.java | 125 +++- .../impl/SettlementAdjustmentServiceImpl.java | 205 +++++++ .../TransportReconciliationServiceImpl.java | 561 ++++++++++++++++++ .../TransportReconciliationWrapper.java | 30 + .../blade_settlement_adjustment_20260818.sql | 39 ++ ...lade_transport_reconciliation_20260818.sql | 82 +++ 37 files changed, 1843 insertions(+), 43 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java create mode 100644 doc/sql/transport/blade_settlement_adjustment_20260818.sql create mode 100644 doc/sql/transport/blade_transport_reconciliation_20260818.sql diff --git a/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java b/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java index 646bbd9..18e2227 100644 --- a/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java +++ b/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java @@ -75,6 +75,10 @@ public class ApiScopePermissionHandler implements IPermissionHandler { if (request == null || user == null) { return false; } + // 超级管理员在菜单授权树中默认拥有全部菜单权限,与框架默认处理器保持一致。 + if (AuthUtil.isAdministrator()) { + return true; + } List codes = permissionMenu(permission, user.getRoleId()); return codes != null && !codes.isEmpty(); } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java index 1140702..d892003 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java @@ -52,6 +52,9 @@ public class PreSettlementSaveRequest implements Serializable { @Schema(description = "合同ID") private Long contractId; + @Schema(description = "结算类型:receivable/payable") + private String settlementType; + @Schema(description = "汇率日期") private LocalDate exchangeRateDate; @@ -67,6 +70,9 @@ public class PreSettlementSaveRequest implements Serializable { @Schema(description = "应收应付明细ID") private List sourceDetailIds; + @Schema(description = "是否允许批量转结算兼容历史来源明细的项目、所属组织或客商差异") + private Boolean allowSourceMismatch; + @Schema(description = "结算合计费用") private List summaryFees; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java new file mode 100644 index 0000000..bf7577f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java @@ -0,0 +1,28 @@ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@Data +public class SettlementAdjustmentSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long formalSettlementId; + private String remark; + private List details; + + @Data + public static class Detail implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementDetailId; + private Long formalSettlementDetailFeeId; + private String feeType; + private String feeItem; + private BigDecimal adjustmentAmountTax; + private BigDecimal adjustmentAmountNoTax; + private String remark; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java new file mode 100644 index 0000000..981293b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java @@ -0,0 +1,12 @@ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +@Data +public class SettlementAdjustmentStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java new file mode 100644 index 0000000..c29dd0a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java @@ -0,0 +1,15 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +/** 运输对账人工匹配请求。 @author Chill */ +@Data +public class TransportReconciliationManualMatchRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Long internalId; + private Long externalId; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java new file mode 100644 index 0000000..be1bcb3 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java @@ -0,0 +1,18 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDate; + +/** 运输对账单保存请求。 @author Chill */ +@Data +public class TransportReconciliationSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long formalSettlementId; + private String reconciliationMode; + private LocalDate reconciliationDate; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java new file mode 100644 index 0000000..08e5977 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java @@ -0,0 +1,35 @@ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_settlement_adjustment") +public class SettlementAdjustment extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String adjustmentNo; + private Long formalSettlementId; + private String formalSettlementNo; + private String settlementType; + private String projectName; + private String deptName; + private String customerName; + private String contractNo; + private String contractName; + private BigDecimal adjustmentAmount; + private BigDecimal originalSettlementAmount; + private BigDecimal adjustedSettlementAmount; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String kingdeeSyncStatus; + private String remark; + private LocalDateTime approvedTime; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java new file mode 100644 index 0000000..bda96fa --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java @@ -0,0 +1,25 @@ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_settlement_adjustment_detail") +public class SettlementAdjustmentDetail extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long adjustmentId; + private Long formalSettlementDetailId; + private Long formalSettlementDetailFeeId; + private String feeType; + private String feeItem; + private BigDecimal originalAmountTax; + private BigDecimal adjustmentAmountTax; + private BigDecimal adjustmentAmountNoTax; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java new file mode 100644 index 0000000..ee00039 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java @@ -0,0 +1,68 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 运输对账单实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation") +@Schema(description = "运输对账单") +public class TransportReconciliation extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String reconciliationNo; + private Long formalSettlementId; + private String formalSettlementNo; + private String preSettlementNos; + private String settlementType; + private String reconciliationMode; + private Long projectId; + private String projectName; + private Long deptId; + private String deptName; + private Long contractId; + private String contractNo; + private String contractName; + private String payerName; + private String payeeName; + private String currency; + private BigDecimal settlementAmount; + private BigDecimal paidAmount; + private Long reconcilerId; + private String reconcilerName; + private LocalDate reconciliationDate; + private String reconciliationStatus; + private String matchStatus; + private Integer internalBillCount; + private Integer externalBillCount; + private Integer differenceCount; + private BigDecimal internalQuantity; + private BigDecimal externalQuantity; + private BigDecimal differenceQuantity; + private BigDecimal internalAmount; + private BigDecimal externalAmount; + private BigDecimal differenceAmount; + private Integer matchedCount; + private Integer unmatchedCount; + private Boolean billUpdated; + private LocalDateTime completedTime; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java new file mode 100644 index 0000000..dd2618b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java @@ -0,0 +1,39 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** 运输对账账单变更记录。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation_change_record") +public class TransportReconciliationChangeRecord extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Long internalDetailId; + private Long formalSettlementId; + private Long formalSettlementDetailId; + private Long sourceDetailId; + private String documentNo; + private String cargoName; + private BigDecimal beforeAmount; + private BigDecimal afterAmount; + private String beforeDataJson; + private String afterDataJson; + private Long operatorId; + private String operatorName; + private LocalDateTime changeTime; + private String changeReason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java new file mode 100644 index 0000000..353febe --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java @@ -0,0 +1,49 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** 运输对账外部账单行。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation_external") +public class TransportReconciliationExternal extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Integer externalLineNo; + private String vehicleNo; + private String departureAddress; + private String arrivalAddress; + private LocalDateTime actualDepartureTime; + private LocalDateTime actualCompletionTime; + private String transportType; + private String cargoName; + private String cargoType; + private String specification; + private String model; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private String batchNo; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal settlementAmount; + private Boolean suspectedDuplicate; + private String matchStatus; + private Long matchedInternalId; + private String errorMessage; + private String rawDataJson; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java new file mode 100644 index 0000000..4ea0058 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java @@ -0,0 +1,55 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** 运输对账内部账单快照。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation_internal") +public class TransportReconciliationInternal extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Long formalSettlementDetailId; + private Long formalSettlementDetailFeeId; + private Long sourceDetailId; + private Long sourceCargoFeeId; + private Integer lineNo; + private String documentNo; + private String waybillNo; + private String vehicleNo; + private String departureAddress; + private String arrivalAddress; + private LocalDateTime actualDepartureTime; + private LocalDateTime actualCompletionTime; + private String transportType; + private String cargoName; + private String cargoType; + private String specification; + private String model; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private String batchNo; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal settlementAmount; + private Long matchedExternalId; + private Integer matchedExternalLineNo; + private String matchResult; + private String updateResult; + private String updateMessage; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java new file mode 100644 index 0000000..3f00b3a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java @@ -0,0 +1,22 @@ +package org.springblade.transport.pojo.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.SettlementAdjustment; +import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; + +import java.time.LocalDate; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class SettlementAdjustmentVO extends SettlementAdjustment { + private LocalDate createStartDate; + private LocalDate createEndDate; + private String approvalStatusName; + private String settlementTypeName; + private String createUserName; + private List details; + private List formalDetails; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java new file mode 100644 index 0000000..5b7ebda --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java @@ -0,0 +1,28 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; +import org.springblade.transport.pojo.entity.TransportReconciliationExternal; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; + +import java.io.Serial; +import java.util.List; + +/** 运输对账单视图实体类。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TransportReconciliationVO extends TransportReconciliation { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String updateUserName; + @TableField(exist = false) private String reconciliationModeName; + @TableField(exist = false) private String reconciliationStatusName; + @TableField(exist = false) private String matchStatusName; + @TableField(exist = false) private List internalDetails; + @TableField(exist = false) private List externalDetails; + @TableField(exist = false) private List changeRecords; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java new file mode 100644 index 0000000..aa4daff --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java @@ -0,0 +1,23 @@ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; +import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest; +import org.springblade.transport.pojo.entity.SettlementAdjustment; +import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; + +import java.util.List; +import java.util.Map; + +public interface ISettlementAdjustmentService { + IPage selectPage(IPage page, SettlementAdjustmentVO query); + SettlementAdjustmentVO detail(Long id); + List> candidateFormalSettlements(String keyword); + List> formalDetails(Long formalSettlementId); + Long saveDraft(SettlementAdjustmentSaveRequest request); + void removeDraft(Long id); + void submit(SettlementAdjustmentStatusRequest request); + void approve(SettlementAdjustmentStatusRequest request); + void returnBill(SettlementAdjustmentStatusRequest request); + String repush(Long adjustmentId); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java index dfa2720..c0bd427 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java @@ -118,11 +118,12 @@ public class ReceivablePayableDetailController extends BladeController { public R>> transferCandidates(Query query, @RequestParam(required = false) String contractName, @RequestParam(required = false) String batchNo, - @RequestParam(required = false) String generateStartDate, - @RequestParam(required = false) String generateEndDate, - @RequestParam(required = false) String settlementBillType) { + @RequestParam(required = false) String generateStartDate, + @RequestParam(required = false) String generateEndDate, + @RequestParam(required = false) String settlementBillType, + @RequestParam(required = false) String settlementType) { return R.data(detailService.transferCandidates(Condition.getPage(query), contractName, batchNo, - generateStartDate, generateEndDate, settlementBillType)); + generateStartDate, generateEndDate, settlementBillType, settlementType)); } @PostMapping("/transfer-settlement") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java new file mode 100644 index 0000000..497ed40 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java @@ -0,0 +1,36 @@ +package org.springblade.transport.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; +import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest; +import org.springblade.transport.pojo.entity.SettlementAdjustment; +import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; +import org.springblade.transport.service.ISettlementAdjustmentService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@AllArgsConstructor +@PreAuth(menu = "settlement_adjustment") +@RequestMapping("/settlement-adjustment") +public class SettlementAdjustmentController extends BladeController { + private final ISettlementAdjustmentService service; + @GetMapping("/list") public R> list(SettlementAdjustmentVO query, Query page) { return R.data(service.selectPage(Condition.getPage(page), query)); } + @GetMapping("/detail") public R detail(@RequestParam Long id) { return R.data(service.detail(id)); } + @GetMapping("/candidate-formal-settlements") public R>> candidates(@RequestParam(required = false) String keyword) { return R.data(service.candidateFormalSettlements(keyword)); } + @GetMapping("/formal-details") public R>> formalDetails(@RequestParam Long formalSettlementId) { return R.data(service.formalDetails(formalSettlementId)); } + @PostMapping("/save") public R save(@RequestBody SettlementAdjustmentSaveRequest request) { return R.data(service.saveDraft(request)); } + @PostMapping("/remove") public R remove(@RequestParam Long id) { service.removeDraft(id); return R.success("删除成功"); } + @PostMapping("/submit") public R submit(@RequestBody SettlementAdjustmentStatusRequest request) { service.submit(request); return R.success("提交成功"); } + @PostMapping("/approve") public R approve(@RequestBody SettlementAdjustmentStatusRequest request) { service.approve(request); return R.success("审批通过"); } + @PostMapping("/return") public R returnBill(@RequestBody SettlementAdjustmentStatusRequest request) { service.returnBill(request); return R.success("已驳回"); } + @PostMapping("/repush") public R repush(@RequestParam Long id) { return R.data(service.repush(id)); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java new file mode 100644 index 0000000..7ca7b65 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java @@ -0,0 +1,144 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.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 lombok.AllArgsConstructor; +import org.springblade.common.excel.ImportFailureExcelUtil; +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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.excel.util.ExcelUtil; +import org.springblade.transport.excel.CargoReconciliationExcel; +import org.springblade.transport.excel.CargoReconciliationFailureExcel; +import org.springblade.transport.excel.VehicleReconciliationExcel; +import org.springblade.transport.excel.VehicleReconciliationFailureExcel; +import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; +import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; +import org.springblade.transport.service.ITransportReconciliationService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +/** 运输对账单控制器。 @author Chill */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "transport_reconciliation") +@RequestMapping("/transport-reconciliation") +@Tag(name = "运输对账", description = "运输对账管理") +public class TransportReconciliationController extends BladeController { + private final ITransportReconciliationService reconciliationService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "运输对账分页") + public R> list(TransportReconciliationVO query, Query pageQuery) { + return R.data(reconciliationService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "运输对账详情") + public R detail(@RequestParam Long id) { return R.data(reconciliationService.detail(id)); } + + @GetMapping("/formal-options") + @ApiOperationSupport(order = 3) + @Operation(summary = "可选正式结算单") + public R> formalOptions(Query pageQuery, @RequestParam(required = false) String settlementType, + @RequestParam(required = false) String keyword) { + return R.data(reconciliationService.formalOptions(Condition.getPage(pageQuery), settlementType, keyword)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 4) + @Operation(summary = "保存运输对账草稿") + public R save(@RequestBody TransportReconciliationSaveRequest request) { return R.data(reconciliationService.saveDraft(request)); } + + @PostMapping("/remove") + @ApiOperationSupport(order = 5) + @Operation(summary = "删除运输对账草稿") + public R remove(@RequestParam Long id) { reconciliationService.removeDraft(id); return R.success("删除成功"); } + + @PostMapping("/import-vehicle") + @ApiOperationSupport(order = 6) + @Operation(summary = "导入整车总额外部账单") + public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { + List failures = reconciliationService.importVehicles(id, ExcelUtil.read(file, VehicleReconciliationExcel.class)); + if (!failures.isEmpty()) { + ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.class); + return null; + } + return R.success("导入数据成功"); + } + + @PostMapping("/import-cargo") + @ApiOperationSupport(order = 7) + @Operation(summary = "导入货物明细外部账单") + public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { + List failures = reconciliationService.importCargoes(id, ExcelUtil.read(file, CargoReconciliationExcel.class)); + if (!failures.isEmpty()) { + ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.class); + return null; + } + return R.success("导入数据成功"); + } + + @GetMapping("/template") + @ApiOperationSupport(order = 8) + @Operation(summary = "下载运输对账模板") + public void template(@RequestParam String mode, HttpServletResponse response) { + if ("cargo".equals(mode)) ExcelUtil.export(response, "货物明细对账模板", "货物明细对账模板", new ArrayList(), CargoReconciliationExcel.class); + else ExcelUtil.export(response, "整车总额对账模板", "整车总额对账模板", new ArrayList(), VehicleReconciliationExcel.class); + } + + @PostMapping("/match") + @ApiOperationSupport(order = 9) + @Operation(summary = "自动匹配内部账单") + public R match(@RequestParam Long id) { reconciliationService.autoMatch(id); return R.success("匹配完成"); } + + @PostMapping("/manual-match") + @ApiOperationSupport(order = 10) + @Operation(summary = "人工匹配账单明细") + public R manualMatch(@RequestBody TransportReconciliationManualMatchRequest request) { reconciliationService.manualMatch(request); return R.success("人工匹配成功"); } + + @PostMapping("/unmatch") + @ApiOperationSupport(order = 11) + @Operation(summary = "取消明细匹配") + public R unmatch(@RequestParam Long internalId) { reconciliationService.unmatch(internalId); return R.success("已取消匹配"); } + + @PostMapping("/adjust") + @ApiOperationSupport(order = 12) + @Operation(summary = "调整内部账单明细") + public R adjust(@RequestBody TransportReconciliationInternal row) { reconciliationService.adjustInternal(row); return R.success("调整成功"); } + + @PostMapping("/update-by-match") + @ApiOperationSupport(order = 13) + @Operation(summary = "按匹配结果更新账单") + public R updateByMatch(@RequestParam Long id) { reconciliationService.updateByMatch(id); return R.success("账单更新完成"); } + + @PostMapping("/complete") + @ApiOperationSupport(order = 14) + @Operation(summary = "完成运输对账") + public R complete(@RequestParam Long id) { reconciliationService.complete(id); return R.success("对账单确认完成"); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java new file mode 100644 index 0000000..a8f55a8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java @@ -0,0 +1,41 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.NumberFormat; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** 货物明细对账导入模型。 @author Chill */ +@Data +@ColumnWidth(22) +public class CargoReconciliationExcel implements Serializable { + @Serial private static final long serialVersionUID = 1L; + @ExcelProperty("车牌号") private String vehicleNo; + @ExcelProperty("发货地址") private String departureAddress; + @ExcelProperty("到货地址") private String arrivalAddress; + @ExcelProperty("实际发货时间") private String actualDepartureTime; + @ExcelProperty("实际完成时间") private String actualCompletionTime; + @ExcelProperty("货物名称") private String cargoName; + @ExcelProperty("货物类型") private String cargoType; + @ExcelProperty("规格") private String specification; + @ExcelProperty("型号") private String model; + @ExcelProperty("运输量") @NumberFormat("0.000000") private BigDecimal transportQuantity; + @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; + @ExcelProperty("里程(KM)") @NumberFormat("0.00") private BigDecimal mileage; + @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; + @ExcelProperty("费用项目名称1") @NumberFormat("0.00") private BigDecimal feeItemOne; + @ExcelProperty("费用项目名称2") @NumberFormat("0.00") private BigDecimal feeItemTwo; + @ExcelProperty("结算金额") @NumberFormat("0.00") private BigDecimal settlementAmount; + @ExcelIgnore private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java new file mode 100644 index 0000000..ffdcb7e --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java @@ -0,0 +1,13 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** 货物明细对账导入失败模型。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CargoReconciliationFailureExcel extends CargoReconciliationExcel { + @ExcelProperty("导入失败原因") private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java new file mode 100644 index 0000000..035d082 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java @@ -0,0 +1,40 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.NumberFormat; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** 整车总额对账导入模型。 @author Chill */ +@Data +@ColumnWidth(22) +public class VehicleReconciliationExcel implements Serializable { + @Serial private static final long serialVersionUID = 1L; + @ExcelProperty("车牌号") private String vehicleNo; + @ExcelProperty("发货地址") private String departureAddress; + @ExcelProperty("到货地址") private String arrivalAddress; + @ExcelProperty("实际发货时间") private String actualDepartureTime; + @ExcelProperty("实际完成时间") private String actualCompletionTime; + @ExcelProperty("运输类型") private String transportType; + @ExcelProperty("货物名称") private String cargoName; + @ExcelProperty("货物类型") private String cargoType; + @ExcelProperty("运输总量") @NumberFormat("0.000000") private BigDecimal transportQuantity; + @ExcelProperty("里程(KM)") @NumberFormat("0.00") private BigDecimal mileage; + @ExcelProperty("批次号") private String batchNo; + @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; + @ExcelProperty("运费") @NumberFormat("0.00") private BigDecimal freightAmount; + @ExcelProperty("费用项目1") @NumberFormat("0.00") private BigDecimal feeItemOne; + @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; + @ExcelIgnore private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java new file mode 100644 index 0000000..5d77df1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java @@ -0,0 +1,13 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** 整车对账导入失败模型。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class VehicleReconciliationFailureExcel extends VehicleReconciliationExcel { + @ExcelProperty("导入失败原因") private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java new file mode 100644 index 0000000..b0e3ba2 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java @@ -0,0 +1,8 @@ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail; + +@Mapper +public interface SettlementAdjustmentDetailMapper extends BaseMapper {} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java new file mode 100644 index 0000000..1a2b562 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java @@ -0,0 +1,8 @@ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.SettlementAdjustment; + +@Mapper +public interface SettlementAdjustmentMapper extends BaseMapper {} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java new file mode 100644 index 0000000..dad962f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; + +/** 运输对账变更记录 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationChangeRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java new file mode 100644 index 0000000..70d9fde --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliationExternal; + +/** 运输对账外部账单 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationExternalMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java new file mode 100644 index 0000000..76049c5 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; + +/** 运输对账内部账单 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationInternalMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java new file mode 100644 index 0000000..1a664e5 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliation; + +/** 运输对账单 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index a500178..bb9aa65 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -58,7 +58,8 @@ public interface IReceivablePayableDetailService extends BaseService> transferCandidates(IPage page, String contractName, String batchNo, - String generateStartDate, String generateEndDate, String settlementBillType); + String generateStartDate, String generateEndDate, String settlementBillType, + String settlementType); IPage> generateWaybills(IPage page, ReceivablePayableGenerateRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java new file mode 100644 index 0000000..a31fca8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java @@ -0,0 +1,39 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.excel.CargoReconciliationExcel; +import org.springblade.transport.excel.CargoReconciliationFailureExcel; +import org.springblade.transport.excel.VehicleReconciliationExcel; +import org.springblade.transport.excel.VehicleReconciliationFailureExcel; +import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; +import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; + +import java.util.List; + +/** 运输对账单服务。 @author Chill */ +public interface ITransportReconciliationService extends BaseService { + IPage selectPage(IPage page, TransportReconciliationVO query); + IPage formalOptions(IPage page, String settlementType, String keyword); + TransportReconciliationVO detail(Long id); + Long saveDraft(TransportReconciliationSaveRequest request); + void removeDraft(Long id); + List importVehicles(Long id, List rows); + List importCargoes(Long id, List rows); + void autoMatch(Long id); + void manualMatch(TransportReconciliationManualMatchRequest request); + void unmatch(Long internalId); + void adjustInternal(TransportReconciliationInternal row); + void updateByMatch(Long id); + void complete(Long id); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index e90abaf..55fc4b1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -272,7 +272,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl requestedIds) { + private void synchronizeDetails(PreSettlement settlement, List requestedIds, boolean allowSourceMismatch) { List distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList(); List existingDetails = listDetails(settlement.getId()); Set requestedSet = new LinkedHashSet<>(distinctIds); @@ -661,7 +661,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl requestedSet.contains(detail.getSourceDetailId())); for (ReceivablePayableDetail source : sources) { - validateCandidate(settlement, source); + validateCandidate(settlement, source, allowSourceMismatch); String sourceCurrency = Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency(); if (!hasRetainedDetail) { settlement.setCurrency(sourceCurrency); @@ -691,16 +691,17 @@ public class PreSettlementServiceImpl extends BaseServiceImpl detailIds = details.stream().map(ReceivablePayableDetail::getId).toList(); + ReceivablePayableDetail first = details.get(0); + if ("pre".equals(request.getSettlementBillType())) { + PreSettlementSaveRequest saveRequest = new PreSettlementSaveRequest(); + saveRequest.setContractId(first.getContractId()); + saveRequest.setSettlementType(first.getSettlementType()); + saveRequest.setSourceDetailIds(detailIds); + saveRequest.setAllowSourceMismatch(true); + preSettlementService.saveDraft(saveRequest); + return; + } + FormalSettlementSaveRequest saveRequest = new FormalSettlementSaveRequest(); + saveRequest.setContractId(first.getContractId()); + saveRequest.setSettlementType(first.getSettlementType()); + saveRequest.setSourceDetailIds(detailIds); + formalSettlementService.saveDraft(saveRequest); + } + + private void validateTransferDetails(List details) { + ReceivablePayableDetail first = details.get(0); + String settlementType = first.getSettlementType(); + Long contractId = first.getContractId(); + if (Func.isEmpty(settlementType) || contractId == null + || details.stream().anyMatch(detail -> Objects.equals(detail.getIsDeleted(), 1) + || !Objects.equals(detail.getSettlementType(), settlementType) + || !Objects.equals(detail.getContractId(), contractId) + || !"pending".equals(detail.getSettlementStatus()) + || Func.isNotEmpty(detail.getPreSettlementNo()) + || Func.isNotEmpty(detail.getFormalSettlementNo()))) { + throw new ServiceException("所选明细必须属于同一合同、结算类型且均为未结算状态"); } } @Override public IPage> transferCandidates(IPage page, String contractName, String batchNo, - String generateStartDate, String generateEndDate, String settlementBillType) { + String generateStartDate, String generateEndDate, String settlementBillType, + String settlementType) { ReceivablePayableDetailVO query = new ReceivablePayableDetailVO(); query.setContractName(contractName); query.setBatchNo(batchNo); query.setSettlementStatus("pending"); + query.setSettlementType(Func.isEmpty(settlementType) ? null : settlementType(settlementType)); query.setGenerateStartDate(parseDate(generateStartDate)); query.setGenerateEndDate(parseDate(generateEndDate)); - IPage detailPage = selectPage(new Page<>(page.getCurrent(), page.getSize()), query); + LambdaQueryWrapper wrapper = buildQuery(query) + .and(item -> item.isNull(ReceivablePayableDetail::getPreSettlementNo) + .or().eq(ReceivablePayableDetail::getPreSettlementNo, "")) + .and(item -> item.isNull(ReceivablePayableDetail::getFormalSettlementNo) + .or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")); + IPage detailPage = ReceivablePayableDetailWrapper.build() + .pageVO(page(new Page<>(page.getCurrent(), page.getSize()), wrapper)); Page> result = new Page<>(detailPage.getCurrent(), detailPage.getSize(), detailPage.getTotal()); - result.setRecords(detailPage.getRecords().stream().map(this::beanMap).toList()); + result.setRecords(detailPage.getRecords().stream().map(this::candidateMap).toList()); return result; } @@ -521,12 +558,12 @@ public class ReceivablePayableDetailServiceImpl BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP); ReceivablePayableDetail detail = new ReceivablePayableDetail(); - detail.setDocumentNo(nextDocumentNo()); + detail.setDocumentNo(nextDocumentNo(settlementType)); detail.setSettlementType(settlementType); - detail.setProjectId(waybill.getProjectId()); - detail.setProjectName(waybill.getProjectName()); - detail.setDeptId(waybill.getDeptId()); - detail.setDeptName(waybill.getDeptName()); + detail.setProjectId(contract == null ? waybill.getProjectId() : contract.getProjectId()); + detail.setProjectName(contract == null ? waybill.getProjectName() : contract.getProjectName()); + detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId()); + detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName()); detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate()); detail.setCustomerName("payable".equals(settlementType) ? (contract == null ? waybill.getCarrierName() : contract.getPartyA()) @@ -926,9 +963,36 @@ public class ReceivablePayableDetailServiceImpl return map; } - private Map beanMap(ReceivablePayableDetailVO detail) { + private Map candidateMap(ReceivablePayableDetailVO detail) { Map map = new LinkedHashMap<>(); - BeanUtil.copyProperties(detail, map); + map.put("id", detail.getId()); + map.put("documentNo", detail.getDocumentNo()); + map.put("settlementType", detail.getSettlementType()); + map.put("projectName", detail.getProjectName()); + map.put("deptName", detail.getDeptName()); + map.put("feeDate", detail.getFeeDate()); + map.put("customerName", detail.getCustomerName()); + map.put("contractNo", detail.getContractNo()); + map.put("contractName", detail.getContractName()); + map.put("preSettlementNo", detail.getPreSettlementNo()); + map.put("formalSettlementNo", detail.getFormalSettlementNo()); + map.put("waybillNo", detail.getWaybillNo()); + map.put("vehicleNo", detail.getVehicleNo()); + map.put("transportType", detail.getTransportType()); + map.put("cargoName", detail.getCargoName()); + map.put("cargoType", detail.getCargoType()); + map.put("transportQuantity", detail.getTransportQuantity()); + map.put("quantityUnit", detail.getQuantityUnit()); + map.put("mileage", detail.getMileage()); + map.put("batchNo", detail.getBatchNo()); + map.put("unitPrice", detail.getUnitPrice()); + map.put("currency", detail.getCurrency()); + map.put("freightAmount", detail.getFreightAmount()); + map.put("otherFeeAmount", detail.getOtherFeeAmount()); + map.put("totalAmount", detail.getTotalAmount()); + map.put("settlementStatus", detail.getSettlementStatus()); + map.put("settlementStatusName", detail.getSettlementStatusName()); + map.put("remark", detail.getRemark()); return map; } @@ -986,12 +1050,9 @@ public class ReceivablePayableDetailServiceImpl return Func.isEmpty(value) ? null : LocalDate.parse(value); } - private synchronized String nextDocumentNo() { - return "YS" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + System.currentTimeMillis() % 100000; - } - - private synchronized String settlementBillNo(String type) { - String prefix = "pre".equals(type) ? "YJ" : "ZJ"; + private synchronized String nextDocumentNo(String settlementType) { + String prefix = "payable".equals(settlementType) ? "YF" : "YS"; return prefix + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + System.currentTimeMillis() % 100000; } + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java new file mode 100644 index 0000000..761ce51 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java @@ -0,0 +1,205 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; +import org.springblade.transport.mapper.FormalSettlementDetailMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.SettlementAdjustmentDetailMapper; +import org.springblade.transport.mapper.SettlementAdjustmentMapper; +import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; +import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.entity.SettlementAdjustment; +import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail; +import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; +import org.springblade.transport.service.ISettlementAdjustmentService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class SettlementAdjustmentServiceImpl extends BaseServiceImpl + implements ISettlementAdjustmentService { + private static final String DRAFT = "draft"; + private static final String REVIEWING = "reviewing"; + private static final String APPROVED = "approved"; + private static final String RETURNED = "returned"; + private final SettlementAdjustmentDetailMapper detailMapper; + private final FormalSettlementMapper formalMapper; + private final FormalSettlementDetailMapper formalDetailMapper; + private final FormalSettlementDetailFeeMapper formalFeeMapper; + + @Override + public IPage selectPage(IPage page, SettlementAdjustmentVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getAdjustmentNo()), SettlementAdjustment::getAdjustmentNo, query.getAdjustmentNo()) + .like(Func.isNotEmpty(query.getFormalSettlementNo()), SettlementAdjustment::getFormalSettlementNo, query.getFormalSettlementNo()) + .like(Func.isNotEmpty(query.getCustomerName()), SettlementAdjustment::getCustomerName, query.getCustomerName()) + .like(Func.isNotEmpty(query.getProjectName()), SettlementAdjustment::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), SettlementAdjustment::getDeptName, query.getDeptName()) + .eq(Func.isNotEmpty(query.getSettlementType()), SettlementAdjustment::getSettlementType, query.getSettlementType()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), SettlementAdjustment::getApprovalStatus, query.getApprovalStatus()) + .ge(query.getCreateStartDate() != null, SettlementAdjustment::getCreateTime, + query.getCreateStartDate() == null ? null : query.getCreateStartDate().atStartOfDay()) + .lt(query.getCreateEndDate() != null, SettlementAdjustment::getCreateTime, + query.getCreateEndDate() == null ? null : query.getCreateEndDate().plusDays(1).atStartOfDay()) + .orderByDesc(SettlementAdjustment::getCreateTime); + return page(page, wrapper).convert(this::toVO); + } + + @Override + public SettlementAdjustmentVO detail(Long id) { + SettlementAdjustment adjustment = existing(id); + SettlementAdjustmentVO vo = toVO(adjustment); + vo.setDetails(detailMapper.selectList(Wrappers.lambdaQuery() + .eq(SettlementAdjustmentDetail::getAdjustmentId, id).orderByAsc(SettlementAdjustmentDetail::getCreateTime))); + vo.setFormalDetails(formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, adjustment.getFormalSettlementId()) + .orderByAsc(FormalSettlementDetail::getLineNo))); + return vo; + } + + @Override + public List> candidateFormalSettlements(String keyword) { + List rows = formalMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlement::getApprovalStatus, APPROVED) + .like(Func.isNotEmpty(keyword), FormalSettlement::getFormalSettlementNo, keyword) + .orderByDesc(FormalSettlement::getCreateTime)); + List> result = new ArrayList<>(); + for (FormalSettlement row : rows) { + Map item = new HashMap<>(); + item.put("id", row.getId()); item.put("formalSettlementNo", row.getFormalSettlementNo()); + item.put("settlementType", row.getSettlementType()); item.put("settlementTypeName", typeName(row.getSettlementType())); + item.put("projectName", row.getProjectName()); item.put("deptName", row.getDeptName()); + item.put("customerName", "receivable".equals(row.getSettlementType()) ? row.getPayerName() : row.getPayeeName()); + item.put("contractNo", row.getContractNo()); item.put("contractName", row.getContractName()); + item.put("settlementAmount", row.getSettlementAmount()); item.put("kingdeeSyncStatus", row.getKingdeeSyncStatus()); + result.add(item); + } + return result; + } + + @Override + public List> formalDetails(Long formalSettlementId) { + FormalSettlement settlement = formalMapper.selectById(formalSettlementId); + if (settlement == null || !APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单可调整"); + List> result = new ArrayList<>(); + List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, formalSettlementId).orderByAsc(FormalSettlementDetail::getLineNo)); + for (FormalSettlementDetail detail : details) { + for (FormalSettlementDetailFee fee : formalFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()).orderByAsc(FormalSettlementDetailFee::getLineNo))) { + Map item = new HashMap<>(); + item.put("formalSettlementDetailId", detail.getId()); item.put("formalSettlementDetailFeeId", fee.getId()); + item.put("documentNo", detail.getDocumentNo()); item.put("cargoName", fee.getCargoName()); + item.put("cargoType", fee.getCargoType()); item.put("feeType", Func.isEmpty(fee.getCargoType()) ? "运输费用" : fee.getCargoType()); + item.put("feeItem", Func.isEmpty(fee.getCargoName()) ? "结算调整" : fee.getCargoName()); item.put("originalAmountTax", fee.getSettlementAmountTax()); + item.put("remark", fee.getRemark()); result.add(item); + } + } + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(SettlementAdjustmentSaveRequest request) { + if (request.getFormalSettlementId() == null) throw new ServiceException("请选择关联正式结算单"); + if (Func.isEmpty(request.getDetails())) throw new ServiceException("请至少添加一条调整费用"); + FormalSettlement formal = formalMapper.selectById(request.getFormalSettlementId()); + if (formal == null || !APPROVED.equals(formal.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单可调整"); + SettlementAdjustment adjustment = request.getId() == null ? new SettlementAdjustment() : editable(request.getId()); + if (adjustment.getId() != null) detailMapper.delete(Wrappers.lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId())); + if (adjustment.getId() == null) { adjustment.setAdjustmentNo(nextNo()); adjustment.setApprovalStatus(DRAFT); } + adjustment.setFormalSettlementId(formal.getId()); adjustment.setFormalSettlementNo(formal.getFormalSettlementNo()); + adjustment.setSettlementType(formal.getSettlementType()); adjustment.setProjectName(formal.getProjectName()); + adjustment.setDeptName(formal.getDeptName()); adjustment.setCustomerName("receivable".equals(formal.getSettlementType()) ? formal.getPayerName() : formal.getPayeeName()); + adjustment.setContractNo(formal.getContractNo()); adjustment.setContractName(formal.getContractName()); + adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); + adjustment.setOriginalSettlementAmount(money(formal.getSettlementAmount())); adjustment.setRemark(limit(request.getRemark(), 200)); + BigDecimal total = BigDecimal.ZERO; + if (request.getDetails() != null) for (SettlementAdjustmentSaveRequest.Detail row : request.getDetails()) { + FormalSettlementDetailFee fee = validateFee(formal.getId(), row.getFormalSettlementDetailId(), row.getFormalSettlementDetailFeeId()); + if (adjustment.getId() == null) save(adjustment); + SettlementAdjustmentDetail detail = new SettlementAdjustmentDetail(); detail.setAdjustmentId(adjustment.getId()); + detail.setFormalSettlementDetailId(row.getFormalSettlementDetailId()); detail.setFormalSettlementDetailFeeId(row.getFormalSettlementDetailFeeId()); + detail.setFeeType(row.getFeeType()); detail.setFeeItem(row.getFeeItem()); detail.setOriginalAmountTax(fee.getSettlementAmountTax()); + detail.setAdjustmentAmountTax(row.getAdjustmentAmountTax() == null ? BigDecimal.ZERO : row.getAdjustmentAmountTax()); + detail.setAdjustmentAmountNoTax(row.getAdjustmentAmountNoTax()); detail.setRemark(limit(row.getRemark(), 200)); detailMapper.insert(detail); + total = total.add(detail.getAdjustmentAmountTax()); + } + adjustment.setAdjustmentAmount(total); adjustment.setAdjustedSettlementAmount(adjustment.getOriginalSettlementAmount().add(total)); saveOrUpdate(adjustment); + return adjustment.getId(); + } + + @Override @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { SettlementAdjustment item = editable(id); detailMapper.delete(Wrappers.lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, id)); removeById(item.getId()); } + @Override public void submit(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), DRAFT, REVIEWING, "审批中", null); } + @Override public void returnBill(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), REVIEWING, RETURNED, "已驳回", limit(request.getReason(), 200)); } + + @Override + @Transactional(rollbackFor = Exception.class) + public String repush(Long adjustmentId) { + SettlementAdjustment adjustment = existing(adjustmentId); + if (!APPROVED.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批通过的结算调整单允许重新推送"); + FormalSettlement formal = formalMapper.selectById(adjustment.getFormalSettlementId()); + if (formal == null || !"synced".equals(formal.getKingdeeSyncStatus())) throw new ServiceException("关联正式结算单尚未推送金蝶,无需重新推送"); + String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + formal.setKingdeeBillNo(kingdeeNo); formal.setSyncedTime(LocalDateTime.now()); formalMapper.updateById(formal); + return kingdeeNo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(SettlementAdjustmentStatusRequest request) { + SettlementAdjustment adjustment = existing(request.getId()); + if (!REVIEWING.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批中的结算调整单允许审核"); + FormalSettlement formal = formalMapper.selectById(adjustment.getFormalSettlementId()); + if (formal == null || !APPROVED.equals(formal.getApprovalStatus())) throw new ServiceException("关联正式结算单状态已变化,无法审批"); + for (SettlementAdjustmentDetail item : detailMapper.selectList(Wrappers.lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId()))) { + FormalSettlementDetailFee fee = validateFee(formal.getId(), item.getFormalSettlementDetailId(), item.getFormalSettlementDetailFeeId()); + fee.setSettlementAmountTax(money(fee.getSettlementAmountTax()).add(money(item.getAdjustmentAmountTax()))); + if (item.getAdjustmentAmountNoTax() != null) fee.setSettlementAmountNoTax(money(fee.getSettlementAmountNoTax()).add(item.getAdjustmentAmountNoTax())); + fee.setAdjustAmount(money(fee.getAdjustAmount()).add(item.getAdjustmentAmountTax())); formalFeeMapper.updateById(fee); + } + for (FormalSettlementDetail detail : formalDetailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()))) { + List fees = formalFeeMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())); + detail.setSettlementAmountTax(fees.stream().map(FormalSettlementDetailFee::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setSettlementAmountNoTax(fees.stream().map(FormalSettlementDetailFee::getSettlementAmountNoTax).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + } + BigDecimal amount = formalDetailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId())).stream().map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + formal.setSettlementAmount(amount); formal.setLocalSettlementAmount(amount.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate())); formalMapper.updateById(formal); + adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); adjustment.setApprovalStatus(APPROVED); adjustment.setCurrentNode("审批通过"); adjustment.setCurrentProcessor(AuthUtil.getUserName()); adjustment.setApprovedTime(LocalDateTime.now()); updateById(adjustment); + } + + private void changeStatus(Long id, String from, String to, String node, String reason) { SettlementAdjustment item = existing(id); if (!from.equals(item.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作"); item.setApprovalStatus(to); item.setCurrentNode(node); if (reason != null) item.setRemark(reason); updateById(item); } + private SettlementAdjustment existing(Long id) { SettlementAdjustment item = getById(id); if (item == null || Objects.equals(item.getIsDeleted(), 1)) throw new ServiceException("结算调整单不存在"); return item; } + private SettlementAdjustment editable(Long id) { SettlementAdjustment item = existing(id); if (!(DRAFT.equals(item.getApprovalStatus()) || RETURNED.equals(item.getApprovalStatus()))) throw new ServiceException("仅草稿或驳回的调整单可编辑"); return item; } + private FormalSettlementDetailFee validateFee(Long formalId, Long detailId, Long feeId) { FormalSettlementDetail detail = formalDetailMapper.selectById(detailId); FormalSettlementDetailFee fee = formalFeeMapper.selectById(feeId); if (detail == null || fee == null || !Objects.equals(detail.getFormalSettlementId(), formalId) || !Objects.equals(fee.getFormalSettlementDetailId(), detailId)) throw new ServiceException("费用明细不存在或不属于关联正式结算单"); return fee; } + private SettlementAdjustmentVO toVO(SettlementAdjustment item) { SettlementAdjustmentVO vo = new SettlementAdjustmentVO(); org.springframework.beans.BeanUtils.copyProperties(item, vo); vo.setCreateUserName(UserCache.getUserRealName(item.getCreateUser())); vo.setApprovalStatusName(statusName(item.getApprovalStatus())); vo.setSettlementTypeName(typeName(item.getSettlementType())); return vo; } + private String statusName(String value) { return Map.of(DRAFT, "草稿", REVIEWING, "审批中", APPROVED, "审批通过", RETURNED, "已驳回").getOrDefault(value, value); } + private String typeName(String value) { return "receivable".equals(value) ? "应收" : "应付"; } + private synchronized String nextNo() { String prefix = "TZ" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = count(Wrappers.lambdaQuery().likeRight(SettlementAdjustment::getAdjustmentNo, prefix)); return prefix + String.format("%04d", count + 1); } + private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("内容不能超过" + max + "个字"); return value; } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java new file mode 100644 index 0000000..7d4a76f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java @@ -0,0 +1,561 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.excel.CargoReconciliationExcel; +import org.springblade.transport.excel.CargoReconciliationFailureExcel; +import org.springblade.transport.excel.VehicleReconciliationExcel; +import org.springblade.transport.excel.VehicleReconciliationFailureExcel; +import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; +import org.springblade.transport.mapper.FormalSettlementDetailMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementSourceMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +import org.springblade.transport.mapper.ReceivablePayableDetailMapper; +import org.springblade.transport.mapper.TransportReconciliationChangeRecordMapper; +import org.springblade.transport.mapper.TransportReconciliationExternalMapper; +import org.springblade.transport.mapper.TransportReconciliationInternalMapper; +import org.springblade.transport.mapper.TransportReconciliationMapper; +import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; +import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; +import org.springblade.transport.pojo.entity.ReceivablePayableDetail; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; +import org.springblade.transport.pojo.entity.TransportReconciliationExternal; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; +import org.springblade.transport.service.ITransportReconciliationService; +import org.springblade.transport.wrapper.TransportReconciliationWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 运输对账单服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class TransportReconciliationServiceImpl + extends BaseServiceImpl + implements ITransportReconciliationService { + + private static final String VEHICLE = "vehicle"; + private static final String CARGO = "cargo"; + private static final String UNFINISHED = "unfinished"; + private static final String COMPLETED = "completed"; + private static final String MATCHED = "matched"; + private static final String UNMATCHED = "unmatched"; + private static final String DUPLICATE = "suspected_duplicate"; + private final FormalSettlementMapper formalSettlementMapper; + private final FormalSettlementSourceMapper formalSourceMapper; + private final FormalSettlementDetailMapper formalDetailMapper; + private final FormalSettlementDetailFeeMapper formalDetailFeeMapper; + private final ReceivablePayableDetailMapper receivablePayableMapper; + private final ReceivablePayableCargoFeeMapper cargoFeeMapper; + private final TransportReconciliationInternalMapper internalMapper; + private final TransportReconciliationExternalMapper externalMapper; + private final TransportReconciliationChangeRecordMapper changeRecordMapper; + + @Override + public IPage selectPage(IPage page, TransportReconciliationVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getReconciliationNo()), TransportReconciliation::getReconciliationNo, query.getReconciliationNo()) + .like(Func.isNotEmpty(query.getFormalSettlementNo()), TransportReconciliation::getFormalSettlementNo, query.getFormalSettlementNo()) + .like(Func.isNotEmpty(query.getPreSettlementNos()), TransportReconciliation::getPreSettlementNos, query.getPreSettlementNos()) + .like(Func.isNotEmpty(query.getProjectName()), TransportReconciliation::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), TransportReconciliation::getDeptName, query.getDeptName()) + .like(Func.isNotEmpty(query.getContractNo()), TransportReconciliation::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getPayerName()), TransportReconciliation::getPayerName, query.getPayerName()) + .like(Func.isNotEmpty(query.getPayeeName()), TransportReconciliation::getPayeeName, query.getPayeeName()) + .eq(Func.isNotEmpty(query.getSettlementType()), TransportReconciliation::getSettlementType, query.getSettlementType()) + .eq(Func.isNotEmpty(query.getMatchStatus()), TransportReconciliation::getMatchStatus, query.getMatchStatus()) + .eq(Func.isNotEmpty(query.getReconciliationStatus()), TransportReconciliation::getReconciliationStatus, query.getReconciliationStatus()) + .orderByDesc(TransportReconciliation::getCreateTime); + return page(page, wrapper).convert(item -> TransportReconciliationWrapper.build().entityVO(item)); + } + + @Override + public IPage formalOptions(IPage page, String settlementType, String keyword) { + List usedIds = list(Wrappers.lambdaQuery() + .select(TransportReconciliation::getFormalSettlementId)) + .stream().map(TransportReconciliation::getFormalSettlementId).filter(Objects::nonNull).toList(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(FormalSettlement::getApprovalStatus, "approved") + .eq(Func.isNotEmpty(settlementType), FormalSettlement::getSettlementType, settlementType) + .and(Func.isNotEmpty(keyword), value -> value.like(FormalSettlement::getFormalSettlementNo, keyword) + .or().like(FormalSettlement::getContractNo, keyword).or().like(FormalSettlement::getContractName, keyword)); + if (!usedIds.isEmpty()) wrapper.notIn(FormalSettlement::getId, usedIds); + return formalSettlementMapper.selectPage(page, wrapper.orderByDesc(FormalSettlement::getCreateTime)); + } + + @Override + public TransportReconciliationVO detail(Long id) { + TransportReconciliationVO vo = TransportReconciliationWrapper.build().entityVO(existing(id)); + vo.setInternalDetails(internalRows(id)); + vo.setExternalDetails(externalRows(id)); + vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.lambdaQuery() + .eq(TransportReconciliationChangeRecord::getReconciliationId, id) + .orderByDesc(TransportReconciliationChangeRecord::getChangeTime))); + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(TransportReconciliationSaveRequest request) { + if (!VEHICLE.equals(request.getReconciliationMode()) && !CARGO.equals(request.getReconciliationMode())) { + throw new ServiceException("请选择正确的对账模式"); + } + FormalSettlement formal = formalSettlementMapper.selectById(request.getFormalSettlementId()); + if (formal == null || !"approved".equals(formal.getApprovalStatus())) throw new ServiceException("请选择已生效的正式结算单"); + long occupied = count(Wrappers.lambdaQuery() + .eq(TransportReconciliation::getFormalSettlementId, formal.getId()) + .ne(request.getId() != null, TransportReconciliation::getId, request.getId())); + if (occupied > 0) throw new ServiceException("该正式结算单已归属其他对账单"); + TransportReconciliation bill = request.getId() == null ? new TransportReconciliation() : editable(request.getId()); + boolean rebuild = bill.getId() == null || !Objects.equals(bill.getFormalSettlementId(), formal.getId()) + || !Objects.equals(bill.getReconciliationMode(), request.getReconciliationMode()); + if (bill.getId() == null) { + bill.setReconciliationNo(nextNo()); + bill.setReconciliationStatus(UNFINISHED); + bill.setMatchStatus(UNMATCHED); + bill.setBillUpdated(false); + } + copyHeader(formal, bill); + bill.setReconciliationMode(request.getReconciliationMode()); + bill.setReconcilerId(AuthUtil.getUserId()); + bill.setReconcilerName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName()); + bill.setReconciliationDate(request.getReconciliationDate() == null ? LocalDate.now() : request.getReconciliationDate()); + bill.setRemark(limit(request.getRemark(), 200)); + saveOrUpdate(bill); + if (rebuild) { + clearDetails(bill.getId()); + buildInternalRows(bill, formal); + } + refreshStats(bill.getId()); + return bill.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + editable(id); + clearDetails(id); + changeRecordMapper.delete(Wrappers.lambdaQuery() + .eq(TransportReconciliationChangeRecord::getReconciliationId, id)); + removeById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List importVehicles(Long id, List rows) { + TransportReconciliation bill = editable(id); + if (!VEHICLE.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是整车总额对账"); + resetExternal(id); + List failures = new ArrayList<>(); + for (int index = 0; index < rows.size(); index++) { + VehicleReconciliationExcel row = rows.get(index); + try { + validateExternal(row.getVehicleNo(), row.getCargoName(), row.getTransportQuantity(), row.getSettlementAmount()); + TransportReconciliationExternal external = new TransportReconciliationExternal(); + BeanUtil.copyProperties(row, external); + external.setReconciliationId(id); external.setExternalLineNo(index + 2); + external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); + external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目1", money(row.getFeeItemOne())))); + external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); + external.setRawDataJson(JsonUtil.toJson(row)); externalMapper.insert(external); + } catch (Exception exception) { + VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel(); + BeanUtil.copyProperties(row, failure); failure.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + failures.add(failure); + } + } + refreshStats(id); + return failures; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List importCargoes(Long id, List rows) { + TransportReconciliation bill = editable(id); + if (!CARGO.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是货物明细对账"); + resetExternal(id); + List failures = new ArrayList<>(); + for (int index = 0; index < rows.size(); index++) { + CargoReconciliationExcel row = rows.get(index); + try { + validateExternal(row.getVehicleNo(), row.getCargoName(), row.getTransportQuantity(), row.getSettlementAmount()); + TransportReconciliationExternal external = new TransportReconciliationExternal(); + BeanUtil.copyProperties(row, external); + external.setReconciliationId(id); external.setExternalLineNo(index + 2); + external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); + external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目名称1", money(row.getFeeItemOne()), "费用项目名称2", money(row.getFeeItemTwo())))); + external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); + external.setRawDataJson(JsonUtil.toJson(row)); externalMapper.insert(external); + } catch (Exception exception) { + CargoReconciliationFailureExcel failure = new CargoReconciliationFailureExcel(); + BeanUtil.copyProperties(row, failure); failure.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + failures.add(failure); + } + } + refreshStats(id); + return failures; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void autoMatch(Long id) { + TransportReconciliation bill = editable(id); + List internals = internalRows(id); + List externals = externalRows(id); + if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); + resetMatches(internals, externals); + Function externalKey = CARGO.equals(bill.getReconciliationMode()) + ? this::cargoKey : this::vehicleKey; + Map> externalGroups = externals.stream().collect(Collectors.groupingBy(externalKey)); + Map> internalGroups = internals.stream().collect(Collectors.groupingBy( + CARGO.equals(bill.getReconciliationMode()) ? this::cargoKey : this::vehicleKey)); + for (Map.Entry> entry : externalGroups.entrySet()) { + List externalGroup = entry.getValue(); + List internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of()); + if (externalGroup.size() == 1 && internalGroup.size() == 1 + && equalMoney(externalGroup.get(0).getSettlementAmount(), internalGroup.get(0).getSettlementAmount())) { + link(internalGroup.get(0), externalGroup.get(0)); + } else if (externalGroup.size() > 1) { + for (TransportReconciliationExternal external : externalGroup) { + external.setSuspectedDuplicate(true); external.setMatchStatus(DUPLICATE); externalMapper.updateById(external); + } + } + } + refreshStats(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void manualMatch(TransportReconciliationManualMatchRequest request) { + editable(request.getReconciliationId()); + TransportReconciliationInternal internal = internalMapper.selectById(request.getInternalId()); + TransportReconciliationExternal external = externalMapper.selectById(request.getExternalId()); + if (internal == null || external == null || !Objects.equals(internal.getReconciliationId(), request.getReconciliationId()) + || !Objects.equals(external.getReconciliationId(), request.getReconciliationId())) throw new ServiceException("匹配明细不存在"); + unlinkInternal(internal); + if (external.getMatchedInternalId() != null) { + TransportReconciliationInternal old = internalMapper.selectById(external.getMatchedInternalId()); + if (old != null) unlinkInternal(old); + } + link(internal, external); + refreshStats(request.getReconciliationId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void unmatch(Long internalId) { + TransportReconciliationInternal internal = internalMapper.selectById(internalId); + if (internal == null) throw new ServiceException("内部账单明细不存在"); + editable(internal.getReconciliationId()); + unlinkInternal(internal); + refreshStats(internal.getReconciliationId()); + } + + @Override + public void adjustInternal(TransportReconciliationInternal row) { + TransportReconciliationInternal internal = internalMapper.selectById(row.getId()); + if (internal == null) throw new ServiceException("内部账单明细不存在"); + editable(internal.getReconciliationId()); + internal.setTransportQuantity(nonNegative(row.getTransportQuantity(), "运输量")); + internal.setUnitPrice(nonNegative(row.getUnitPrice(), "运输单价")); + internal.setMileage(nonNegative(row.getMileage(), "里程")); + internal.setFreightAmount(nonNegative(row.getFreightAmount(), "运输费")); + internal.setFeeItemsJson(row.getFeeItemsJson()); + internal.setSettlementAmount(nonNegative(row.getSettlementAmount(), "结算金额")); + internal.setUpdateResult("manually_adjusted"); + internalMapper.updateById(internal); + refreshStats(internal.getReconciliationId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateByMatch(Long id) { + TransportReconciliation bill = editable(id); + List internals = assertAllMatched(bill); + if (money(bill.getExternalAmount()).compareTo(money(bill.getPaidAmount())) < 0) { + throw new ServiceException("导入账单匹配金额小于已付金额,不能更新内部账单"); + } + Map externalMap = externalRows(id).stream() + .collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity())); + for (TransportReconciliationInternal internal : internals) { + TransportReconciliationExternal external = externalMap.get(internal.getMatchedExternalId()); + if (VEHICLE.equals(bill.getReconciliationMode()) && hasMultipleCargo(internal.getFormalSettlementDetailId())) { + internal.setUpdateResult("skipped_multi_cargo"); + internal.setUpdateMessage("一车多货,已跳过自动更新,请人工调整货物费用"); + internalMapper.updateById(internal); + continue; + } + applyAmount(bill, internal, external); + } + recalculateSettlement(bill); + bill.setBillUpdated(true); + updateById(bill); + refreshStats(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void complete(Long id) { + TransportReconciliation bill = editable(id); + assertAllMatched(bill); + refreshStats(id); + bill = existing(id); + if (bill.getDifferenceCount() != 0 || money(bill.getDifferenceQuantity()).compareTo(BigDecimal.ZERO) != 0 + || money(bill.getDifferenceAmount()).compareTo(BigDecimal.ZERO) != 0) { + throw new ServiceException("差异单数、差异货量和差异金额必须全部为0才可完成对账"); + } + bill.setReconciliationStatus(COMPLETED); + bill.setCompletedTime(LocalDateTime.now()); + updateById(bill); + } + + private void buildInternalRows(TransportReconciliation bill, FormalSettlement formal) { + List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()).orderByAsc(FormalSettlementDetail::getLineNo)); + int lineNo = 1; + for (FormalSettlementDetail detail : details) { + List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()).orderByAsc(FormalSettlementDetailFee::getLineNo)); + if (CARGO.equals(bill.getReconciliationMode()) && !fees.isEmpty()) { + for (FormalSettlementDetailFee fee : fees) insertInternal(bill.getId(), detail, fee, lineNo++); + } else { + insertInternal(bill.getId(), detail, null, lineNo++); + } + } + } + + private void insertInternal(Long billId, FormalSettlementDetail detail, FormalSettlementDetailFee fee, int lineNo) { + TransportReconciliationInternal row = new TransportReconciliationInternal(); + BeanUtil.copyProperties(detail, row); + row.setId(null); row.setReconciliationId(billId); row.setFormalSettlementDetailId(detail.getId()); + row.setSourceDetailId(detail.getSourceDetailId()); row.setLineNo(lineNo); row.setMatchResult(UNMATCHED); row.setUpdateResult("not_updated"); + if (fee != null) { + row.setFormalSettlementDetailFeeId(fee.getId()); row.setSourceCargoFeeId(fee.getSourceFeeId()); + row.setCargoName(fee.getCargoName()); row.setCargoType(fee.getCargoType()); row.setTransportQuantity(fee.getTransportQuantity()); + row.setQuantityUnit(fee.getQuantityUnit()); row.setMileage(fee.getMileage()); row.setUnitPrice(fee.getUnitPrice()); + row.setFreightAmount(fee.getFreightAmount()); row.setFeeItemsJson(fee.getFeeItemsJson()); row.setSettlementAmount(fee.getSettlementAmountTax()); + ReceivablePayableCargoFee sourceFee = fee.getSourceFeeId() == null ? null : cargoFeeMapper.selectById(fee.getSourceFeeId()); + if (sourceFee != null) { row.setSpecification(sourceFee.getSpecification()); row.setModel(sourceFee.getModel()); } + } else { + row.setSettlementAmount(detail.getSettlementAmountTax()); + } + internalMapper.insert(row); + } + + private void applyAmount(TransportReconciliation bill, TransportReconciliationInternal internal, TransportReconciliationExternal external) { + BigDecimal before = money(internal.getSettlementAmount()); + BigDecimal after = money(external.getSettlementAmount()); + if (internal.getFormalSettlementDetailFeeId() != null) { + FormalSettlementDetailFee fee = formalDetailFeeMapper.selectById(internal.getFormalSettlementDetailFeeId()); + fee.setSettlementAmountTax(after); fee.setAdjustAmount(after.subtract(money(fee.getOriginalAmount()))); formalDetailFeeMapper.updateById(fee); + if (internal.getSourceCargoFeeId() != null) { + ReceivablePayableCargoFee sourceFee = cargoFeeMapper.selectById(internal.getSourceCargoFeeId()); + if (sourceFee != null) { sourceFee.setAfterAmount(after); sourceFee.setAdjustAmount(after.subtract(money(sourceFee.getOriginalAmount()))); cargoFeeMapper.updateById(sourceFee); } + } + } else { + FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); + detail.setSettlementAmountTax(after); detail.setAdjustAmount(after.subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + ReceivablePayableDetail source = receivablePayableMapper.selectById(internal.getSourceDetailId()); + if (source != null) { source.setTotalAmount(after); receivablePayableMapper.updateById(source); } + } + internal.setSettlementAmount(after); internal.setUpdateResult("updated"); internal.setUpdateMessage("已按外部账单更新"); internalMapper.updateById(internal); + TransportReconciliationChangeRecord record = new TransportReconciliationChangeRecord(); + record.setReconciliationId(bill.getId()); record.setInternalDetailId(internal.getId()); record.setFormalSettlementId(bill.getFormalSettlementId()); + record.setFormalSettlementDetailId(internal.getFormalSettlementDetailId()); record.setSourceDetailId(internal.getSourceDetailId()); + record.setDocumentNo(internal.getDocumentNo()); record.setCargoName(internal.getCargoName()); record.setBeforeAmount(before); record.setAfterAmount(after); + record.setBeforeDataJson(JsonUtil.toJson(Map.of("settlementAmount", before))); record.setAfterDataJson(JsonUtil.toJson(external)); + record.setOperatorId(AuthUtil.getUserId()); record.setOperatorName(AuthUtil.getUserName()); record.setChangeTime(LocalDateTime.now()); + record.setChangeReason("运输对账按匹配结果更新"); changeRecordMapper.insert(record); + } + + private void recalculateSettlement(TransportReconciliation bill) { + List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, bill.getFormalSettlementId())); + for (FormalSettlementDetail detail : details) { + List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())); + if (!fees.isEmpty()) { + BigDecimal total = fees.stream().map(FormalSettlementDetailFee::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + detail.setSettlementAmountTax(total); detail.setAdjustAmount(total.subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + ReceivablePayableDetail source = receivablePayableMapper.selectById(detail.getSourceDetailId()); + if (source != null) { source.setTotalAmount(total); receivablePayableMapper.updateById(source); } + } + } + BigDecimal total = details.stream().map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + FormalSettlement formal = formalSettlementMapper.selectById(bill.getFormalSettlementId()); + formal.setSettlementAmount(total); formal.setLocalSettlementAmount(total.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate())); + formalSettlementMapper.updateById(formal); + bill.setSettlementAmount(total); + } + + private List assertAllMatched(TransportReconciliation bill) { + List internals = internalRows(bill.getId()); + List externals = externalRows(bill.getId()); + if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size() + || internals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchResult())) + || externals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchStatus()) || Boolean.TRUE.equals(item.getSuspectedDuplicate()))) { + throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复"); + } + return internals; + } + + private void refreshStats(Long id) { + TransportReconciliation bill = existing(id); + List internals = internalRows(id); + List externals = externalRows(id); + int matched = (int) internals.stream().filter(item -> MATCHED.equals(item.getMatchResult())).count(); + bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size()); + int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).count(); + int externalUnmatched = (int) externals.stream().filter(item -> !MATCHED.equals(item.getMatchStatus())).count(); + bill.setMatchedCount(matched); bill.setUnmatchedCount(internalUnmatched + externalUnmatched); + bill.setDifferenceCount(Math.abs(internals.size() - externals.size()) + Math.min(internalUnmatched, externalUnmatched)); + bill.setInternalQuantity(sumInternalQuantity(internals)); bill.setExternalQuantity(sumExternalQuantity(externals)); + bill.setDifferenceQuantity(bill.getInternalQuantity().subtract(bill.getExternalQuantity()).abs()); + bill.setInternalAmount(sumInternalAmount(internals)); bill.setExternalAmount(sumExternalAmount(externals)); + bill.setDifferenceAmount(bill.getInternalAmount().subtract(bill.getExternalAmount()).abs()); + bill.setMatchStatus(internals.size() > 0 && internals.size() == externals.size() && matched == internals.size() ? MATCHED : matched > 0 ? "partial" : UNMATCHED); + updateById(bill); + } + + private void link(TransportReconciliationInternal internal, TransportReconciliationExternal external) { + internal.setMatchedExternalId(external.getId()); internal.setMatchedExternalLineNo(external.getExternalLineNo()); internal.setMatchResult(MATCHED); internalMapper.updateById(internal); + external.setMatchedInternalId(internal.getId()); external.setMatchStatus(MATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); + } + + private void unlinkInternal(TransportReconciliationInternal internal) { + if (internal.getMatchedExternalId() != null) { + TransportReconciliationExternal external = externalMapper.selectById(internal.getMatchedExternalId()); + if (external != null) { external.setMatchedInternalId(null); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); } + } + internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); + } + + private void resetMatches(List internals, List externals) { + for (TransportReconciliationInternal internal : internals) { internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); } + for (TransportReconciliationExternal external : externals) { external.setMatchedInternalId(null); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); } + } + + private void resetExternal(Long id) { + List internals = internalRows(id); + for (TransportReconciliationInternal internal : internals) { internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); } + externalMapper.delete(Wrappers.lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id)); + } + + private void clearDetails(Long id) { + internalMapper.delete(Wrappers.lambdaQuery().eq(TransportReconciliationInternal::getReconciliationId, id)); + externalMapper.delete(Wrappers.lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id)); + } + + private void copyHeader(FormalSettlement formal, TransportReconciliation bill) { + bill.setFormalSettlementId(formal.getId()); bill.setFormalSettlementNo(formal.getFormalSettlementNo()); bill.setSettlementType(formal.getSettlementType()); + bill.setProjectId(formal.getProjectId()); bill.setProjectName(formal.getProjectName()); bill.setDeptId(formal.getDeptId()); bill.setDeptName(formal.getDeptName()); + bill.setContractId(formal.getContractId()); bill.setContractNo(formal.getContractNo()); bill.setContractName(formal.getContractName()); + bill.setPayerName(formal.getPayerName()); bill.setPayeeName(formal.getPayeeName()); bill.setCurrency(formal.getCurrency()); + bill.setSettlementAmount(money(formal.getSettlementAmount())); bill.setPaidAmount(money(formal.getPaidAmount())); + List preNos = formalSourceMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, formal.getId())) + .stream().map(FormalSettlementSource::getPreSettlementNo).filter(Func::isNotEmpty).toList(); + bill.setPreSettlementNos(String.join(",", preNos)); + } + + private TransportReconciliation existing(Long id) { + TransportReconciliation bill = getById(id); + if (bill == null) throw new ServiceException("运输对账单不存在"); + return bill; + } + + private TransportReconciliation editable(Long id) { + TransportReconciliation bill = existing(id); + if (!UNFINISHED.equals(bill.getReconciliationStatus())) throw new ServiceException("已完成的运输对账单禁止修改或删除"); + return bill; + } + + private List internalRows(Long id) { + return internalMapper.selectList(Wrappers.lambdaQuery().eq(TransportReconciliationInternal::getReconciliationId, id).orderByAsc(TransportReconciliationInternal::getLineNo)); + } + + private List externalRows(Long id) { + return externalMapper.selectList(Wrappers.lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id).orderByAsc(TransportReconciliationExternal::getExternalLineNo)); + } + + private boolean hasMultipleCargo(Long formalDetailId) { + return formalDetailFeeMapper.selectCount(Wrappers.lambdaQuery().eq(FormalSettlementDetailFee::getFormalSettlementDetailId, formalDetailId)) > 1; + } + + private void validateExternal(String vehicleNo, String cargoName, BigDecimal quantity, BigDecimal amount) { + if (Func.isEmpty(vehicleNo)) throw new ServiceException("车牌号不能为空"); + if (Func.isEmpty(cargoName)) throw new ServiceException("货物名称不能为空"); + nonNegative(quantity, "运输量"); nonNegative(amount, "结算金额"); + } + + private LocalDateTime parseTime(String value, String field) { + if (Func.isEmpty(value)) throw new ServiceException(field + "不能为空"); + LocalDateTime result = parseTimeNullable(value, field); + if (result == null) throw new ServiceException(field + "格式错误"); + return result; + } + + private LocalDateTime parseTimeNullable(String value, String field) { + if (Func.isEmpty(value)) return null; + for (String pattern : List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm")) { + try { return LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)); } catch (DateTimeParseException ignored) { } + } + throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss"); + } + + private String vehicleKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), row.getCargoName(), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); } + private String vehicleKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), row.getCargoName(), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); } + private String cargoKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getActualDepartureTime(), row.getTransportQuantity()); } + private String cargoKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getActualDepartureTime(), row.getTransportQuantity()); } + private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); } + private String normal(Object value) { if (value == null) return ""; if (value instanceof BigDecimal decimal) return decimal.stripTrailingZeros().toPlainString(); return value.toString().trim().replaceAll("\\s+", "").toLowerCase(); } + private boolean equalMoney(BigDecimal left, BigDecimal right) { return money(left).compareTo(money(right)) == 0; } + private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; } + private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("备注不能超过" + max + "个字"); return value; } + private BigDecimal sumInternalQuantity(List rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal sumExternalQuantity(List rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal sumInternalAmount(List rows) { return rows.stream().map(TransportReconciliationInternal::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal sumExternalAmount(List rows) { return rows.stream().map(TransportReconciliationExternal::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private String nextNo() { return "DZ" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java new file mode 100644 index 0000000..c794d95 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java @@ -0,0 +1,30 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; + +import java.util.Objects; + +/** 运输对账单包装类。 @author Chill */ +public class TransportReconciliationWrapper extends BaseEntityWrapper { + public static TransportReconciliationWrapper build() { return new TransportReconciliationWrapper(); } + + @Override + public TransportReconciliationVO entityVO(TransportReconciliation entity) { + TransportReconciliationVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, TransportReconciliationVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setReconciliationModeName("cargo".equals(entity.getReconciliationMode()) ? "货物明细" : "整车总额"); + vo.setReconciliationStatusName("completed".equals(entity.getReconciliationStatus()) ? "已完成" : "未完成"); + vo.setMatchStatusName(switch (entity.getMatchStatus() == null ? "" : entity.getMatchStatus()) { + case "matched" -> "已匹配"; + case "partial" -> "部分匹配"; + default -> "未匹配"; + }); + return vo; + } +} diff --git a/doc/sql/transport/blade_settlement_adjustment_20260818.sql b/doc/sql/transport/blade_settlement_adjustment_20260818.sql new file mode 100644 index 0000000..5c63d71 --- /dev/null +++ b/doc/sql/transport/blade_settlement_adjustment_20260818.sql @@ -0,0 +1,39 @@ +-- 结算管理 / 结算调整单 +CREATE TABLE IF NOT EXISTS `blade_settlement_adjustment` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `adjustment_no` varchar(100) NOT NULL COMMENT '结算调整单号', `formal_settlement_id` bigint(20) NOT NULL COMMENT '关联正式结算单ID', + `formal_settlement_no` varchar(100) NOT NULL COMMENT '关联正式结算单号', `settlement_type` varchar(30) NOT NULL, + `project_name` varchar(100) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `customer_name` varchar(200) DEFAULT NULL, + `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(100) DEFAULT NULL, + `adjustment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `original_settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原结算金额', + `adjusted_settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整后结算金额', + `approval_status` varchar(30) NOT NULL DEFAULT 'draft', `current_node` varchar(100) DEFAULT NULL, + `current_processor` varchar(200) DEFAULT NULL, `kingdee_sync_status` varchar(30) DEFAULT NULL COMMENT '关联正式单金蝶状态', `remark` varchar(200) DEFAULT NULL, `approved_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), UNIQUE KEY `uk_settlement_adjustment_no` (`tenant_id`,`adjustment_no`), + KEY `idx_adjustment_formal` (`formal_settlement_id`), KEY `idx_adjustment_status` (`approval_status`), KEY `idx_adjustment_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='结算调整单'; + +CREATE TABLE IF NOT EXISTS `blade_settlement_adjustment_detail` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `adjustment_id` bigint(20) NOT NULL, `formal_settlement_detail_id` bigint(20) NOT NULL, `formal_settlement_detail_fee_id` bigint(20) NOT NULL, + `fee_type` varchar(100) DEFAULT NULL, `fee_item` varchar(200) DEFAULT NULL, `original_amount_tax` decimal(18,2) DEFAULT NULL, + `adjustment_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00', `adjustment_amount_no_tax` decimal(18,2) DEFAULT NULL, + `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_adjustment_detail_bill` (`adjustment_id`), + KEY `idx_adjustment_detail_fee` (`formal_settlement_detail_fee_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='结算调整费用明细'; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000001060,2090000000000001000,'settlement_adjustment','结算调整单','settlement_adjustment','/settlement/settlement-adjustment','iconfont icon-caidanguanli',5,1,0,1,NULL,'',0), +(2090000000000001061,2090000000000001060,'settlement_adjustment_view','查看','settlement_adjustment_view','','',1,2,0,1,NULL,'',0), +(2090000000000001062,2090000000000001060,'settlement_adjustment_add','新增','settlement_adjustment_add','','',2,2,0,1,NULL,'',0), +(2090000000000001063,2090000000000001060,'settlement_adjustment_edit','编辑','settlement_adjustment_edit','','',3,2,0,1,NULL,'',0), +(2090000000000001064,2090000000000001060,'settlement_adjustment_delete','删除','settlement_adjustment_delete','','',4,2,0,1,NULL,'',0), +(2090000000000001065,2090000000000001060,'settlement_adjustment_submit','提交审批','settlement_adjustment_submit','','',5,2,0,1,NULL,'',0), +(2090000000000001066,2090000000000001060,'settlement_adjustment_approve','审批','settlement_adjustment_approve','','',6,2,0,1,NULL,'',0), +(2090000000000001067,2090000000000001060,'settlement_adjustment_repush','重新推送金蝶','settlement_adjustment_repush','','',7,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_transport_reconciliation_20260818.sql b/doc/sql/transport/blade_transport_reconciliation_20260818.sql new file mode 100644 index 0000000..9c539e1 --- /dev/null +++ b/doc/sql/transport/blade_transport_reconciliation_20260818.sql @@ -0,0 +1,82 @@ +-- 结算管理 / 运输对账 +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_no` varchar(100) NOT NULL, `formal_settlement_id` bigint(20) NOT NULL, `formal_settlement_no` varchar(100) NOT NULL, + `pre_settlement_nos` varchar(1000) DEFAULT NULL, `settlement_type` varchar(30) NOT NULL, `reconciliation_mode` varchar(30) NOT NULL, + `project_id` bigint(20) DEFAULT NULL, `project_name` varchar(100) DEFAULT NULL, `dept_id` bigint(20) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, + `contract_id` bigint(20) DEFAULT NULL, `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(200) DEFAULT NULL, + `payer_name` varchar(200) DEFAULT NULL, `payee_name` varchar(200) DEFAULT NULL, `currency` varchar(20) DEFAULT 'RMB', + `settlement_amount` decimal(18,2) DEFAULT 0, `paid_amount` decimal(18,2) DEFAULT 0, `reconciler_id` bigint(20) DEFAULT NULL, + `reconciler_name` varchar(100) DEFAULT NULL, `reconciliation_date` date DEFAULT NULL, `reconciliation_status` varchar(30) NOT NULL DEFAULT 'unfinished', + `match_status` varchar(30) NOT NULL DEFAULT 'unmatched', `internal_bill_count` int(11) DEFAULT 0, `external_bill_count` int(11) DEFAULT 0, + `difference_count` int(11) DEFAULT 0, `internal_quantity` decimal(18,6) DEFAULT 0, `external_quantity` decimal(18,6) DEFAULT 0, + `difference_quantity` decimal(18,6) DEFAULT 0, `internal_amount` decimal(18,2) DEFAULT 0, `external_amount` decimal(18,2) DEFAULT 0, + `difference_amount` decimal(18,2) DEFAULT 0, `matched_count` int(11) DEFAULT 0, `unmatched_count` int(11) DEFAULT 0, + `bill_updated` tinyint(1) DEFAULT 0, `completed_time` datetime DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), UNIQUE KEY `uk_transport_reconciliation_no` (`tenant_id`,`reconciliation_no`), + KEY `idx_transport_reconciliation_formal` (`formal_settlement_id`), KEY `idx_transport_reconciliation_type` (`settlement_type`), + KEY `idx_transport_reconciliation_status` (`reconciliation_status`,`match_status`), KEY `idx_transport_reconciliation_create` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账单'; + +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation_internal` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_id` bigint(20) NOT NULL, `formal_settlement_detail_id` bigint(20) NOT NULL, `formal_settlement_detail_fee_id` bigint(20) DEFAULT NULL, + `source_detail_id` bigint(20) DEFAULT NULL, `source_cargo_fee_id` bigint(20) DEFAULT NULL, `line_no` int(11) NOT NULL, + `document_no` varchar(100) DEFAULT NULL, `waybill_no` varchar(100) DEFAULT NULL, `vehicle_no` varchar(100) DEFAULT NULL, + `departure_address` varchar(500) DEFAULT NULL, `arrival_address` varchar(500) DEFAULT NULL, `actual_departure_time` datetime DEFAULT NULL, + `actual_completion_time` datetime DEFAULT NULL, `transport_type` varchar(100) DEFAULT NULL, `cargo_name` varchar(500) DEFAULT NULL, + `cargo_type` varchar(500) DEFAULT NULL, `specification` varchar(200) DEFAULT NULL, `model` varchar(200) DEFAULT NULL, + `transport_quantity` decimal(18,6) DEFAULT NULL, `quantity_unit` varchar(50) DEFAULT NULL, `mileage` decimal(18,2) DEFAULT NULL, + `batch_no` varchar(100) DEFAULT NULL, `unit_price` decimal(18,2) DEFAULT NULL, `freight_amount` decimal(18,2) DEFAULT 0, + `fee_items_json` longtext, `settlement_amount` decimal(18,2) DEFAULT 0, `matched_external_id` bigint(20) DEFAULT NULL, + `matched_external_line_no` int(11) DEFAULT NULL, `match_result` varchar(30) DEFAULT 'unmatched', `update_result` varchar(30) DEFAULT 'not_updated', + `update_message` varchar(500) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_recon_internal_bill` (`reconciliation_id`), + KEY `idx_recon_internal_source` (`source_detail_id`), KEY `idx_recon_internal_match` (`matched_external_id`), + KEY `idx_recon_internal_vehicle` (`vehicle_no`,`cargo_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账内部账单快照'; + +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation_external` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_id` bigint(20) NOT NULL, `external_line_no` int(11) NOT NULL, `vehicle_no` varchar(100) DEFAULT NULL, + `departure_address` varchar(500) DEFAULT NULL, `arrival_address` varchar(500) DEFAULT NULL, `actual_departure_time` datetime DEFAULT NULL, + `actual_completion_time` datetime DEFAULT NULL, `transport_type` varchar(100) DEFAULT NULL, `cargo_name` varchar(500) DEFAULT NULL, + `cargo_type` varchar(500) DEFAULT NULL, `specification` varchar(200) DEFAULT NULL, `model` varchar(200) DEFAULT NULL, + `transport_quantity` decimal(18,6) DEFAULT NULL, `quantity_unit` varchar(50) DEFAULT NULL, `mileage` decimal(18,2) DEFAULT NULL, + `batch_no` varchar(100) DEFAULT NULL, `unit_price` decimal(18,2) DEFAULT NULL, `freight_amount` decimal(18,2) DEFAULT 0, + `fee_items_json` longtext, `settlement_amount` decimal(18,2) DEFAULT 0, `suspected_duplicate` tinyint(1) DEFAULT 0, + `match_status` varchar(30) DEFAULT 'unmatched', `matched_internal_id` bigint(20) DEFAULT NULL, `error_message` varchar(500) DEFAULT NULL, + `raw_data_json` longtext, PRIMARY KEY (`id`), KEY `idx_recon_external_bill` (`reconciliation_id`), KEY `idx_recon_external_line` (`reconciliation_id`,`external_line_no`), + KEY `idx_recon_external_match` (`matched_internal_id`), KEY `idx_recon_external_status` (`match_status`,`suspected_duplicate`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账外部账单'; + +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation_change_record` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_id` bigint(20) NOT NULL, `internal_detail_id` bigint(20) DEFAULT NULL, `formal_settlement_id` bigint(20) DEFAULT NULL, + `formal_settlement_detail_id` bigint(20) DEFAULT NULL, `source_detail_id` bigint(20) DEFAULT NULL, `document_no` varchar(100) DEFAULT NULL, + `cargo_name` varchar(500) DEFAULT NULL, `before_amount` decimal(18,2) DEFAULT 0, `after_amount` decimal(18,2) DEFAULT 0, + `before_data_json` longtext, `after_data_json` longtext, `operator_id` bigint(20) DEFAULT NULL, `operator_name` varchar(100) DEFAULT NULL, + `change_time` datetime DEFAULT NULL, `change_reason` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), + KEY `idx_recon_change_bill` (`reconciliation_id`), KEY `idx_recon_change_source` (`source_detail_id`), KEY `idx_recon_change_time` (`change_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账账单变更记录'; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000002060,2090000000000001000,'transport_reconciliation','运输对账','transport_reconciliation','/settlement/transport-reconciliation','iconfont icon-caidanguanli',5,1,0,1,NULL,'',0), +(2090000000000002061,2090000000000002060,'transport_reconciliation_view','查看','transport_reconciliation_view','', '',1,2,0,1,NULL,'',0), +(2090000000000002062,2090000000000002060,'transport_reconciliation_add','新增','transport_reconciliation_add','', '',2,2,0,1,NULL,'',0), +(2090000000000002063,2090000000000002060,'transport_reconciliation_edit','编辑','transport_reconciliation_edit','', '',3,2,0,1,NULL,'',0), +(2090000000000002064,2090000000000002060,'transport_reconciliation_delete','删除','transport_reconciliation_delete','', '',4,2,0,1,NULL,'',0), +(2090000000000002065,2090000000000002060,'transport_reconciliation_import','导入外部账单','transport_reconciliation_import','', '',5,2,0,1,NULL,'',0), +(2090000000000002066,2090000000000002060,'transport_reconciliation_match','开始匹配','transport_reconciliation_match','', '',6,2,0,1,NULL,'',0), +(2090000000000002067,2090000000000002060,'transport_reconciliation_update','按匹配结果更新','transport_reconciliation_update','', '',7,2,0,1,NULL,'',0), +(2090000000000002068,2090000000000002060,'transport_reconciliation_complete','完成对账','transport_reconciliation_complete','', '',8,2,0,1,NULL,'',0), +(2090000000000002069,2090000000000002060,'transport_reconciliation_export','导出','transport_reconciliation_export','', '',9,2,0,1,NULL,'',0), +(2090000000000002070,2090000000000002060,'transport_reconciliation_adjust','明细调整','transport_reconciliation_adjust','', '',10,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; From 8beb542c9881afaafa316f9e83ab64a730214ede Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 20 Aug 2026 06:30:50 +0800 Subject: [PATCH 030/114] fix bug --- .../ReceivablePayableAdjustFeeRequest.java | 15 + .../ReceivablePayableDetailController.java | 62 ++- .../service/ILoadingManageService.java | 3 + .../IReceivablePayableDetailService.java | 6 + .../impl/LoadingManageServiceImpl.java | 45 ++- .../service/impl/MasterOrderServiceImpl.java | 11 +- .../impl/PreSettlementServiceImpl.java | 19 +- .../ReceivablePayableDetailServiceImpl.java | 373 +++++++++++++++++- .../impl/ShippingTemplateServiceImpl.java | 2 +- .../service/impl/WaybillServiceImpl.java | 54 ++- 10 files changed, 558 insertions(+), 32 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java index e434b2a..e34efbc 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java @@ -55,5 +55,20 @@ public class ReceivablePayableAdjustFeeRequest implements Serializable { @Schema(description = "动态费用项目") private Map feeItems; + + @Schema(description = "是否手工费用行") + private Boolean manualFee; + + @Schema(description = "手工费用项目名称") + private String feeItemName; + + @Schema(description = "手工费用类型:charge/deduct") + private String feeType; + + @Schema(description = "手工费用金额") + private BigDecimal amount; + + @Schema(description = "备注") + private String remark; } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java index c0bd427..f72c499 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java @@ -24,12 +24,12 @@ package org.springblade.transport.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import cn.idev.excel.FastExcel; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import org.springblade.core.boot.ctrl.BladeController; -import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; @@ -43,6 +43,7 @@ import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; import org.springblade.transport.service.IReceivablePayableDetailService; +import org.springblade.system.cache.DictCache; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -51,7 +52,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; +import java.math.BigDecimal; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; /** * 应收应付明细控制器 @@ -160,7 +167,56 @@ public class ReceivablePayableDetailController extends BladeController { @ApiOperationSupport(order = 10) @Operation(summary = "导出应收应付明细") public void exportReceivablePayableDetail(ReceivablePayableDetailVO query, HttpServletResponse response) { - IPage page = detailService.selectPage(Condition.getPage(new Query()), query); - ExcelUtil.export(response, "应收应付明细" + DateUtil.time(), "应收应付明细", page.getRecords(), ReceivablePayableDetailVO.class); + List records = detailService.selectList(query); + Set feeItemNames = new LinkedHashSet<>(); + records.forEach(row -> { + if (row.getFeeItems() != null) feeItemNames.addAll(row.getFeeItems().keySet()); + }); + + List> head = new ArrayList<>(); + String[] baseHeaders = {"单据号", "项目名称", "所属组织", "费用日期", "客商名称", "合同编号", "合同名称", "来源", + "预结算单号", "正式结算单号", "运单号", "车号", "运输类型", "货物名称", "货物类型", "运输总量", "里程(KM)", + "批次号", "运输单价"}; + for (String header : baseHeaders) head.add(List.of(header)); + feeItemNames.forEach(name -> head.add(List.of(name))); + for (String header : new String[] {"费用合计", "状态", "创建人", "创建时间"}) head.add(List.of(header)); + + List> rows = records.stream().map(row -> { + List values = new ArrayList<>(); + values.add(row.getDocumentNo()); values.add(row.getProjectName()); values.add(row.getDeptName()); values.add(row.getFeeDate()); + values.add(row.getCustomerName()); values.add(row.getContractNo()); values.add(row.getContractName()); values.add(row.getSourceType()); + values.add(row.getPreSettlementNo()); values.add(row.getFormalSettlementNo()); values.add(row.getWaybillNo()); values.add(row.getVehicleNo()); + values.add(transportTypeName(row.getTransportType())); values.add(row.getCargoName()); values.add(row.getCargoType()); + values.add(row.getTransportQuantity()); values.add(row.getMileage() != null && row.getMileage().compareTo(BigDecimal.valueOf(-1)) == 0 ? null : row.getMileage()); + values.add(row.getBatchNo()); values.add(money(row.getUnitPrice(), row.getCurrency())); + feeItemNames.forEach(name -> values.add(money(decimal(row.getFeeItems() == null ? null : row.getFeeItems().get(name)), row.getCurrency()))); + values.add(money(row.getTotalAmount(), row.getCurrency())); values.add(row.getSettlementStatusName()); values.add(row.getCreateUserName()); values.add(row.getCreateTime()); + return values; + }).toList(); + + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode("应收应付明细" + DateUtil.time(), StandardCharsets.UTF_8) + ".xlsx"); + try { + FastExcel.write(response.getOutputStream()).head(head).sheet("应收应付明细").doWrite(rows); + } catch (Exception exception) { + throw new IllegalStateException("导出应收应付明细失败", exception); + } + } + + private String transportTypeName(String value) { + if (value == null || value.isBlank()) return value; + String name = DictCache.getValue("transport_type", value); + return name == null || name.isBlank() ? value : name; + } + + private String money(BigDecimal value, String currency) { + if (value == null) return "-"; + return value.setScale(2, java.math.RoundingMode.HALF_UP).toPlainString() + " " + (currency == null || currency.isBlank() ? "RMB" : currency); + } + + private BigDecimal decimal(Object value) { + if (value == null || String.valueOf(value).isBlank()) return null; + try { return new BigDecimal(String.valueOf(value)); } catch (NumberFormatException ignored) { return null; } } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index 43bfaa4..50452e8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -42,6 +42,9 @@ public interface ILoadingManageService extends BaseService { boolean complete(Long id); + /** 运单完成后检查关联运单状态,全部完成时自动完成配载单。 */ + boolean completeIfAllWaybillsCompleted(String loadingNo); + BusinessRemoveResultVO batchComplete(String ids); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index bb9aa65..1dc0ab8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -28,6 +28,7 @@ import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; +import org.springblade.transport.pojo.entity.MasterOrder; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; @@ -45,6 +46,8 @@ public interface IReceivablePayableDetailService extends BaseService selectPage(IPage page, ReceivablePayableDetailVO query); + List selectList(ReceivablePayableDetailVO query); + ReceivablePayableFeeDetailVO feeDetail(Long id); IPage changeRecords(IPage page, Long detailId); @@ -69,4 +72,7 @@ public interface IReceivablePayableDetailService extends BaseService waybillIds); + + /** 关闭总单调度后按合同系统计费模式自动生成总单应收、应付明细。 */ + void generateForClosedMasterOrder(MasterOrder masterOrder); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 09396f5..436e050 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -263,8 +263,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpllambdaQuery() + .eq(LoadingManage::getLoadingNo, loadingNo) + .eq(LoadingManage::getIsDeleted, 0) + .last("FOR UPDATE"), false); + if (loadingManage == null + || Objects.equals(loadingManage.getBusinessStatus(), STATUS_COMPLETED) + || Objects.equals(loadingManage.getBusinessStatus(), STATUS_CANCELLED) + || Objects.equals(loadingManage.getBusinessStatus(), STATUS_DRAFT)) { + return false; + } + List waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); + if (Func.isEmpty(waybillIdList)) { + return false; + } + List waybillList = waybillMapper.selectList(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .in(Waybill::getId, waybillIdList)); + boolean allCompleted = waybillList.size() == waybillIdList.size() + && waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_COMPLETED)); + if (!allCompleted) { + return false; + } + loadingManage.setBusinessStatus(STATUS_COMPLETED); + return updateById(loadingManage); + } + @Override @Transactional(rollbackFor = Exception.class) public BusinessRemoveResultVO batchComplete(String ids) { @@ -292,7 +324,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); if (Func.isEmpty(waybillIdList)) { return false; @@ -300,8 +332,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillList = waybillMapper.selectList(Wrappers.lambdaQuery() .eq(Waybill::getIsDeleted, 0) .in(Waybill::getId, waybillIdList)); - return waybillList.size() == waybillIdList.size() - && waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_RUNNING)); + if (waybillList.size() != waybillIdList.size()) { + return false; + } + return waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_RUNNING)) + || waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_COMPLETED)); } private LambdaQueryWrapper buildQuery(LoadingManageVO loadingManage) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index 3a15e5e..8ec2c59 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -24,6 +24,7 @@ import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.service.IMasterOrderService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IProjectApplyService; +import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.ITransportPlanService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.TransportBusinessSupport; @@ -53,13 +54,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl existingSourceIds = existingDetails.stream().map(PreSettlementDetail::getSourceDetailId) .collect(Collectors.toSet()); List addedIds = distinctIds.stream().filter(id -> !existingSourceIds.contains(id)).toList(); + List addedSources = new ArrayList<>(); if (!addedIds.isEmpty()) { List sources = sourceDetailMapper.selectBatchIds(addedIds); if (sources.size() != addedIds.size()) { @@ -683,12 +684,20 @@ public class PreSettlementServiceImpl extends BaseServiceImpl detailLineMap = listDetails(settlement.getId()).stream() + .collect(Collectors.toMap(PreSettlementDetail::getSourceDetailId, PreSettlementDetail::getLineNo, + (left, right) -> left)); + for (ReceivablePayableDetail source : addedSources) { + saveChange(settlement.getId(), "结算明细项", detailLineMap.get(source.getId()), "新增", + "新增单据号" + source.getDocumentNo(), ""); + } + } } private void validateCandidate(PreSettlement settlement, ReceivablePayableDetail source, @@ -883,7 +892,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl implements IReceivablePayableDetailService { + private static final String SOURCE_MASTER_ORDER = "总单系统生成"; + private final ReceivablePayableCargoFeeMapper cargoFeeMapper; private final ReceivablePayableChangeRecordMapper changeRecordMapper; + private final MasterOrderMapper masterOrderMapper; private final IWaybillService waybillService; private final IContractManageService contractManageService; private final ICommonAddressService commonAddressService; @@ -101,6 +106,7 @@ public class ReceivablePayableDetailServiceImpl public ReceivablePayableDetailServiceImpl(ReceivablePayableCargoFeeMapper cargoFeeMapper, ReceivablePayableChangeRecordMapper changeRecordMapper, + MasterOrderMapper masterOrderMapper, IWaybillService waybillService, IContractManageService contractManageService, ICommonAddressService commonAddressService, @@ -108,6 +114,7 @@ public class ReceivablePayableDetailServiceImpl @Lazy IFormalSettlementService formalSettlementService) { this.cargoFeeMapper = cargoFeeMapper; this.changeRecordMapper = changeRecordMapper; + this.masterOrderMapper = masterOrderMapper; this.waybillService = waybillService; this.contractManageService = contractManageService; this.commonAddressService = commonAddressService; @@ -120,6 +127,11 @@ public class ReceivablePayableDetailServiceImpl return ReceivablePayableDetailWrapper.build().pageVO(page(page, buildQuery(query))); } + @Override + public List selectList(ReceivablePayableDetailVO query) { + return ReceivablePayableDetailWrapper.build().listVO(list(buildQuery(query))); + } + @Override public ReceivablePayableFeeDetailVO feeDetail(Long id) { ReceivablePayableDetail detail = getExisting(id); @@ -127,6 +139,10 @@ public class ReceivablePayableDetailServiceImpl .eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) .eq(ReceivablePayableCargoFee::getIsDeleted, 0) .orderByAsc(ReceivablePayableCargoFee::getCreateTime)); + // 运输量的单位取明细对应运单单位,兼容历史费用行误存计费单位的情况。 + if (Func.isNotEmpty(detail.getQuantityUnit())) { + rows.forEach(row -> row.setQuantityUnit(detail.getQuantityUnit())); + } ReceivablePayableFeeDetailVO result = buildFeeDetail(rows); LinkedHashSet feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); feeItemNames.addAll(result.getFeeItemNames()); @@ -157,6 +173,9 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(request.getContractId())) { throw new ServiceException("请选择需要更新费用的合同"); } + if (Func.isEmpty(request.getBillingPlanId())) { + throw new ServiceException("请选择需要更新的合同计费方案"); + } List details = list(buildUpdateQuery(request)); if (Func.isEmpty(details)) { throw new ServiceException("没有可更新的待结算明细"); @@ -217,14 +236,65 @@ public class ReceivablePayableDetailServiceImpl .eq(ReceivablePayableCargoFee::getIsDeleted, 0)); Map existingMap = existingRows.stream() .collect(java.util.stream.Collectors.toMap(ReceivablePayableCargoFee::getId, row -> row)); - if (request.getRows().size() != existingRows.size()) { + Set submittedExistingIds = request.getRows().stream() + .map(ReceivablePayableAdjustFeeRequest.AdjustRow::getId) + .filter(Objects::nonNull) + .collect(java.util.stream.Collectors.toSet()); + if (!submittedExistingIds.equals(existingMap.keySet())) { throw new ServiceException("费用调整行数据不完整"); } Set allowedFeeItems = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); existingRows.forEach(row -> allowedFeeItems.addAll(parseMap(row.getFeeItemsJson()).keySet())); List changes = new ArrayList<>(); + List allRows = new ArrayList<>(existingRows); for (ReceivablePayableAdjustFeeRequest.AdjustRow adjusted : request.getRows()) { ReceivablePayableCargoFee existing = existingMap.get(adjusted.getId()); + if (adjusted.getRemark() != null && adjusted.getRemark().length() > 200) { + throw new ServiceException("备注不能超过200个字"); + } + if (Boolean.TRUE.equals(adjusted.getManualFee())) { + if (Func.isEmpty(adjusted.getFeeItemName())) { + throw new ServiceException("请填写手工费用项目"); + } + if (!List.of("charge", "deduct").contains(adjusted.getFeeType())) { + throw new ServiceException("手工费用类型不正确"); + } + validateNonNegative(adjusted.getAmount(), "手工费用金额"); + BigDecimal signedAmount = money(adjusted.getAmount()) + .multiply("deduct".equals(adjusted.getFeeType()) ? BigDecimal.valueOf(-1) : BigDecimal.ONE); + Map manualItems = new LinkedHashMap<>(); + manualItems.put(adjusted.getFeeItemName().trim(), signedAmount); + boolean newManualRow = existing == null; + if (newManualRow) { + existing = new ReceivablePayableCargoFee(); + existing.setDetailId(detail.getId()); + existing.setLineNo("ADJ-" + System.currentTimeMillis()); + existing.setOriginalAmount(BigDecimal.ZERO); + } + String oldName = existing.getCargoName(); + BigDecimal oldAmount = money(existing.getAfterAmount()); + // cargoName 表示运单货物名称,手工收费项名称仅保存到费用项目 JSON。 + existing.setCargoName(detail.getCargoName()); + existing.setBillingFactor("手工调整"); + existing.setBillingType("deduct".equals(adjusted.getFeeType()) ? "手工扣费" : "手工收费"); + existing.setTransportQuantity(detail.getTransportQuantity()); + existing.setQuantityUnit(detail.getQuantityUnit()); + existing.setMileage(detail.getMileage()); + existing.setFreightAmount(BigDecimal.ZERO); + existing.setFeeItemsJson(JsonUtil.toJson(manualItems)); + existing.setAdjustAmount(signedAmount.subtract(money(existing.getOriginalAmount()))); + existing.setAfterAmount(signedAmount); + existing.setRemark(adjusted.getRemark()); + if (newManualRow) { + cargoFeeMapper.insert(existing); + allRows.add(existing); + } else { + cargoFeeMapper.updateById(existing); + } + changes.add("【手工费用】从[" + oldName + " " + formatValue(oldAmount) + "]调整为[" + + existing.getBillingType() + " " + existing.getCargoName() + " " + formatValue(signedAmount) + "]"); + continue; + } if (existing == null) { throw new ServiceException("存在无效的费用调整行"); } @@ -244,6 +314,10 @@ public class ReceivablePayableDetailServiceImpl appendChange(changes, "计费数量", existing.getTransportQuantity(), adjusted.getTransportQuantity()); appendChange(changes, "里程", existing.getMileage(), adjusted.getMileage()); appendChange(changes, "运输费", existing.getFreightAmount(), adjusted.getFreightAmount()); + if (!Objects.equals(existing.getRemark(), adjusted.getRemark())) { + changes.add("【备注】从[" + Objects.toString(existing.getRemark(), "") + "]调整为[" + + Objects.toString(adjusted.getRemark(), "") + "]"); + } Map oldFeeItems = parseMap(existing.getFeeItemsJson()); for (String name : allowedFeeItems) { appendChange(changes, name, decimal(oldFeeItems.get(name)), money(feeItems.get(name))); @@ -254,6 +328,7 @@ public class ReceivablePayableDetailServiceImpl existing.setMileage(money(adjusted.getMileage())); existing.setFreightAmount(freightAmount); existing.setFeeItemsJson(JsonUtil.toJson(feeItems)); + existing.setRemark(adjusted.getRemark()); existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount()))); existing.setAfterAmount(afterAmount); cargoFeeMapper.updateById(existing); @@ -264,7 +339,7 @@ public class ReceivablePayableDetailServiceImpl for (int i = 0; i < changes.size(); i++) { saveChangeRecord(detail, changes.get(i), request.getAdjustReason(), String.format("%04d", i + 1)); } - refreshAdjustedDetail(detail, existingRows); + refreshAdjustedDetail(detail, allRows); } @Override @@ -420,6 +495,50 @@ public class ReceivablePayableDetailServiceImpl } } + @Override + @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, + rollbackFor = Exception.class) + public void generateForClosedMasterOrder(MasterOrder masterOrder) { + if (masterOrder == null || Func.isEmpty(masterOrder.getId()) || Func.isEmpty(masterOrder.getContractId())) { + return; + } + ContractManage contract = contractManageService.getById(masterOrder.getContractId()); + if (contract == null || !isSystemGeneration(contract)) return; + try { + List masterGoods = masterOrderGoods(masterOrder); + if (masterGoods.isEmpty()) return; + List matchedFees = new ArrayList<>(); + for (Waybill masterGoodsItem : masterGoods) { + String planId = matchedPlanId(masterGoodsItem, contract); + if (Func.isEmpty(planId)) continue; + matchedFees.addAll(calculatedFees(masterGoodsItem, contract, planId, true)); + } + if (matchedFees.isEmpty()) return; + normalizeMasterFeeLines(matchedFees); + BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); + for (String settlementType : List.of("payable", "receivable")) { + if (existsByMasterOrder(masterOrder.getMasterNo(), settlementType)) continue; + ReceivablePayableDetail detail = buildMasterOrderDetail(masterOrder, contract, masterGoods, + settlementType, matchedFees, contractUnitPrice); + save(detail); + for (ReceivablePayableCargoFee fee : matchedFees) { + ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); + copy.setId(null); + copy.setDetailId(detail.getId()); + copy.setWaybillId(null); + cargoFeeMapper.insert(copy); + } + } + } catch (Exception exception) { + log.error("自动生成总单费用明细失败,masterOrderId:{}, masterNo:{}, contractId:{}, failureReason:{}", + masterOrder.getId(), masterOrder.getMasterNo(), masterOrder.getContractId(), exception.getMessage(), exception); + if (exception instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new RuntimeException(exception); + } + } + private boolean isSystemGeneration(ContractManage contract) { if (Func.isNotEmpty(contract.getFeeGenerationMode())) { return "system".equalsIgnoreCase(contract.getFeeGenerationMode()) || "系统生成".equals(contract.getFeeGenerationMode()); @@ -525,9 +644,12 @@ public class ReceivablePayableDetailServiceImpl LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(Waybill::getIsDeleted, 0) .eq(Waybill::getContractId, request.getContractId()) - .eq(Waybill::getBusinessStatus, "completed") - .notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail where is_deleted = 0" - + (Func.isNotEmpty(request.getSettlementType()) ? " and settlement_type = '" + settlementType(request.getSettlementType()) + "'" : "")); + .eq(Waybill::getBusinessStatus, "completed"); + String targetSettlementType = settlementType(request.getSettlementType()); + wrapper.notInSql(Waybill::getId, + "select waybill_id from blade_receivable_payable_detail" + + " where is_deleted = 0 and waybill_id is not null and settlement_type = '" + + targetSettlementType + "'"); if (Func.isNotEmpty(request.getBatchNo())) { wrapper.like(Waybill::getBatchNo, request.getBatchNo()); } @@ -553,7 +675,7 @@ public class ReceivablePayableDetailServiceImpl } private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String settlementType, - List fees, BigDecimal unitPrice) { + List fees, BigDecimal unitPrice) { BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP); @@ -580,7 +702,7 @@ public class ReceivablePayableDetailServiceImpl detail.setCargoType(waybill.getCargoType()); detail.setTransportQuantity(waybill.getQuantity()); detail.setQuantityUnit(waybill.getQuantityUnit()); - detail.setMileage(waybill.getMileage()); + detail.setMileage(normalizeGeneratedMileage(waybill.getMileage())); detail.setBatchNo(waybill.getBatchNo()); detail.setUnitPrice(unitPrice); detail.setCurrency("RMB"); @@ -593,6 +715,80 @@ public class ReceivablePayableDetailServiceImpl return detail; } + private ReceivablePayableDetail buildMasterOrderDetail(MasterOrder masterOrder, ContractManage contract, + List masterGoods, String settlementType, + List fees, BigDecimal unitPrice) { + Waybill masterWaybill = masterGoods.get(0); + ReceivablePayableDetail detail = buildDetail(masterWaybill, contract, settlementType, fees, unitPrice); + detail.setSourceType(SOURCE_MASTER_ORDER); + detail.setWaybillId(null); + detail.setWaybillNo(masterOrder.getMasterNo()); + detail.setVehicleNo(null); + detail.setTransportType(masterOrder.getTransportOrganizationType()); + detail.setCargoName(joinMasterGoodsField(masterGoods, Waybill::getCargoName)); + detail.setCargoType(joinMasterGoodsField(masterGoods, Waybill::getCargoType)); + detail.setTransportQuantity(masterGoods.stream().map(Waybill::getQuantity) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setQuantityUnit(commonMasterGoodsValue(masterGoods, Waybill::getQuantityUnit)); + detail.setMileage(null); + detail.setBatchNo(null); + detail.setRemark(masterOrder.getRemark()); + return detail; + } + + private List masterOrderGoods(MasterOrder masterOrder) { + List result = new ArrayList<>(); + for (Map goods : parseList(masterOrder.getGoodsJson())) { + BigDecimal quantity = decimal(goods.get("quantity")); + if (quantity.compareTo(BigDecimal.ZERO) <= 0) continue; + Waybill masterGoodsItem = new Waybill(); + masterGoodsItem.setProjectId(masterOrder.getProjectId()); + masterGoodsItem.setProjectName(masterOrder.getProjectName()); + masterGoodsItem.setContractId(masterOrder.getContractId()); + masterGoodsItem.setContractName(masterOrder.getContractName()); + masterGoodsItem.setCustomerName(masterOrder.getCustomerName()); + masterGoodsItem.setTransportType(masterOrder.getTransportOrganizationType()); + masterGoodsItem.setCargoName(stringValue(goods, "cargoName")); + masterGoodsItem.setCargoType(stringValue(goods, "cargoType")); + masterGoodsItem.setSpecification(stringValue(goods, "specification")); + masterGoodsItem.setModel(stringValue(goods, "model")); + masterGoodsItem.setQuantity(quantity); + masterGoodsItem.setQuantityUnit(stringValue(goods, "quantityUnit")); + masterGoodsItem.setDepartureName(masterOrder.getDepartureName()); + masterGoodsItem.setDepartureAddress(masterOrder.getDepartureAddress()); + masterGoodsItem.setArrivalName(masterOrder.getArrivalName()); + masterGoodsItem.setArrivalAddress(masterOrder.getArrivalAddress()); + masterGoodsItem.setEndDate(masterOrder.getPlanEndTime() == null ? LocalDate.now() + : masterOrder.getPlanEndTime().toLocalDate()); + masterGoodsItem.setMasterNo(masterOrder.getMasterNo()); + masterGoodsItem.setWaybillNo(masterOrder.getMasterNo()); + masterGoodsItem.setGoodsJson(JsonUtil.toJson(goods)); + masterGoodsItem.setRemark(masterOrder.getRemark()); + result.add(masterGoodsItem); + } + return result; + } + + private void normalizeMasterFeeLines(List fees) { + for (int index = 0; index < fees.size(); index++) { + ReceivablePayableCargoFee fee = fees.get(index); + fee.setWaybillId(null); + fee.setLineNo(String.format("%04d", index + 1)); + } + } + + private String joinMasterGoodsField(List masterGoods, + java.util.function.Function getter) { + return masterGoods.stream().map(getter).filter(Func::isNotEmpty).distinct() + .collect(java.util.stream.Collectors.joining(",")); + } + + private String commonMasterGoodsValue(List masterGoods, + java.util.function.Function getter) { + List values = masterGoods.stream().map(getter).filter(Func::isNotEmpty).distinct().toList(); + return values.size() == 1 ? values.get(0) : ""; + } + private BigDecimal resolveContractUnitPrice(List fees) { return fees.stream() .filter(this::isFreight) @@ -631,7 +827,7 @@ public class ReceivablePayableDetailServiceImpl cargoFee.setQuantityUnit(waybill.getQuantityUnit()); cargoFee.setPriceUnit(waybill.getPriceUnit()); cargoFee.setUnitPrice(unitPrice); - cargoFee.setMileage(waybill.getMileage()); + cargoFee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); cargoFee.setFreightAmount(freightAmount); cargoFee.setFeeItemsJson(JsonUtil.toJson(feeItems)); cargoFee.setOriginalAmount(total); @@ -647,9 +843,7 @@ public class ReceivablePayableDetailServiceImpl private List calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) { List> plans = parseList(contract == null ? null : contract.getBillingPlanJson()); - Map plan = "__matched__".equals(planId) ? plans.stream().filter(this::isDefaultPlan).findFirst().orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1)) : plans.stream().filter(item -> Objects.equals(stringValue(item, "id"), planId) - || Objects.equals(stringValue(item, "planId"), planId)).findFirst() - .orElseGet(() -> plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null)); + Map plan = resolveBillingPlan(plans, planId); if (plan == null || !(plan.get("rules") instanceof List)) return matchOnly ? List.of() : List.of(buildCargoFee(null, waybill)); List result = new ArrayList<>(); int line = 1; @@ -664,9 +858,9 @@ public class ReceivablePayableDetailServiceImpl fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType()); fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); - fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(stringValue(rule, "billingUnit", waybill.getQuantityUnit())); + fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(waybill.getQuantityUnit()); fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); - fee.setMileage(waybill.getMileage()); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); + fee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount))); fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); result.add(fee); @@ -674,6 +868,24 @@ public class ReceivablePayableDetailServiceImpl return result.isEmpty() && !matchOnly ? List.of(buildCargoFee(null, waybill)) : result; } + private Map resolveBillingPlan(List> plans, String planId) { + if ("__matched__".equals(planId)) { + return plans.stream().filter(this::isDefaultPlan).findFirst() + .orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1)); + } + if (Func.isEmpty(planId)) { + return plans.stream().filter(this::isDefaultPlan).findFirst() + .orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1)); + } + return plans.stream().filter(plan -> Objects.equals(stringValue(plan, "id"), planId) + || Objects.equals(stringValue(plan, "planId"), planId) + || Objects.equals(stringValue(plan, "name"), planId) + || Objects.equals(stringValue(plan, "planName"), planId) + || Objects.equals(stringValue(plan, "billingPlanName"), planId)) + .findFirst() + .orElseThrow(() -> new ServiceException("合同计费方案不存在或已变更,请重新选择")); + } + private boolean isDefaultPlan(Map plan) { Object value = plan.get("defaultPlan"); return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(String.valueOf(value)); @@ -851,6 +1063,10 @@ public class ReceivablePayableDetailServiceImpl } private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) { + if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) { + rebuildMasterOrderDetailFee(detail, billingPlanId); + return; + } Waybill waybill = waybillService.getById(detail.getWaybillId()); if (waybill == null) { throw new ServiceException("关联运单不存在"); @@ -869,6 +1085,38 @@ public class ReceivablePayableDetailServiceImpl updateById(detail); } + private void rebuildMasterOrderDetailFee(ReceivablePayableDetail detail, String billingPlanId) { + MasterOrder masterOrder = masterOrderMapper.selectOne(Wrappers.lambdaQuery() + .eq(MasterOrder::getMasterNo, detail.getWaybillNo()) + .eq(MasterOrder::getIsDeleted, 0)); + if (masterOrder == null) { + throw new ServiceException("关联总单不存在"); + } + ContractManage contract = contractManageService.getById(detail.getContractId()); + List masterGoods = masterOrderGoods(masterOrder); + List fees = masterGoods.stream() + .flatMap(goods -> calculatedFees(goods, contract, billingPlanId).stream()).toList(); + normalizeMasterFeeLines(fees); + cargoFeeMapper.delete(Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); + fees.forEach(fee -> { + fee.setDetailId(detail.getId()); + fee.setWaybillId(null); + cargoFeeMapper.insert(fee); + }); + BigDecimal freight = fees.stream().filter(this::isFreight) + .map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + detail.setFreightAmount(freight); + detail.setOtherFeeAmount(total.subtract(freight)); + detail.setTotalAmount(total); + detail.setUnitPrice(resolveContractUnitPrice(fees)); + detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, + (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); + updateById(detail); + } + private void closeDetails(List ids, String settlementType) { if (Func.isEmpty(ids)) { throw new ServiceException("请选择需要关闭的明细"); @@ -927,6 +1175,14 @@ public class ReceivablePayableDetailServiceImpl .eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0; } + private boolean existsByMasterOrder(String masterNo, String settlementType) { + return count(Wrappers.lambdaQuery() + .eq(ReceivablePayableDetail::getSourceType, SOURCE_MASTER_ORDER) + .eq(ReceivablePayableDetail::getWaybillNo, masterNo) + .eq(ReceivablePayableDetail::getSettlementType, settlementType) + .eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0; + } + private String settlementType(String value) { if (Func.isEmpty(value)) return "receivable"; if (!List.of("receivable", "payable").contains(value)) { @@ -960,9 +1216,96 @@ public class ReceivablePayableDetailServiceImpl map.put("transportType", waybill.getTransportType()); map.put("carrierType", waybill.getCarrierType()); map.put("cargoInfo", waybill.getCargoName()); + map.put("departureAddress", waybill.getDepartureAddress()); + map.put("departureContact", waybill.getDepartureContact()); + map.put("arrivalAddress", waybill.getArrivalAddress()); + map.put("arrivalContact", waybill.getArrivalContact()); + map.put("unitPrice", waybill.getUnitPrice()); + map.put("freight", waybillFreight(waybill)); + map.put("otherFeeTotal", waybillOtherFee(waybill)); + map.put("freightTotal", waybillFreightTotal(waybill)); + map.put("contractName", waybill.getContractName()); + map.put("planName", waybill.getPlanName()); + map.put("batchNo", waybill.getBatchNo()); + map.put("originalNo", waybill.getOriginalNo()); + map.put("masterNo", waybill.getMasterNo()); + map.put("remark", Func.isNotEmpty(waybill.getRemark()) ? waybill.getRemark() : waybill.getTaskRemark()); + map.put("createTime", waybill.getCreateTime()); + map.put("updateTime", waybill.getUpdateTime()); + map.put("businessStatusName", waybillStatusName(waybill.getBusinessStatus())); return map; } + private BigDecimal waybillFreight(Waybill waybill) { + Map freight = waybillFreightMap(waybill); + Object value = firstValue(freight, "freight", "freightAmount", "transportFee"); + if (value != null) return decimal(value); + if (freight.get("freightItems") instanceof List items) { + BigDecimal total = BigDecimal.ZERO; + boolean hasAmount = false; + for (Object item : items) { + if (!(item instanceof Map source)) continue; + Object amount = firstValue(stringMap(source), "freightAmount", "amount", "totalAmount"); + if (amount == null) continue; + total = total.add(decimal(amount)); + hasAmount = true; + } + if (hasAmount) return total; + } + return null; + } + + private BigDecimal waybillOtherFee(Waybill waybill) { + Map freight = waybillFreightMap(waybill); + Object value = firstValue(freight, "otherFeeTotal", "otherFreightAmount", "otherAmount"); + return value == null ? waybill.getOtherFeeTotal() : decimal(value); + } + + private BigDecimal waybillFreightTotal(Waybill waybill) { + Map freight = waybillFreightMap(waybill); + Object total = firstValue(freight, "freightTotal", "totalFreight", "totalFreightAmount", "totalAmount"); + if (total != null) return decimal(total); + BigDecimal freightAmount = waybillFreight(waybill); + BigDecimal otherFeeAmount = waybillOtherFee(waybill); + if (freightAmount == null && otherFeeAmount == null) return null; + return money(freightAmount).add(money(otherFeeAmount)); + } + + private Map waybillFreightMap(Waybill waybill) { + if (Func.isEmpty(waybill.getFreightJson())) return new LinkedHashMap<>(); + try { + Object parsed = JsonUtil.parse(waybill.getFreightJson(), Object.class); + if (parsed instanceof Map source) return stringMap(source); + if (parsed instanceof List list && !list.isEmpty() && list.get(0) instanceof Map source) { + return stringMap(source); + } + } catch (Exception ignored) { + // 兼容历史费用 JSON 异常数据。 + } + return new LinkedHashMap<>(); + } + + private Map stringMap(Map source) { + Map result = new LinkedHashMap<>(); + source.forEach((key, value) -> result.put(String.valueOf(key), value)); + return result; + } + + private Object firstValue(Map map, String... keys) { + for (String key : keys) if (map.get(key) != null && !String.valueOf(map.get(key)).isBlank()) return map.get(key); + return null; + } + + private String waybillStatusName(String status) { + return switch (status == null ? "" : status) { + case "completed" -> "已完成"; + case "processing", "running", "in_progress", "inProgress" -> "进行中"; + case "pending", "created" -> "待执行"; + case "cancelled", "canceled" -> "已取消"; + default -> status; + }; + } + private Map candidateMap(ReceivablePayableDetailVO detail) { Map map = new LinkedHashMap<>(); map.put("id", detail.getId()); @@ -1038,6 +1381,10 @@ public class ReceivablePayableDetailServiceImpl return value == null ? BigDecimal.ZERO : value.setScale(2, RoundingMode.HALF_UP); } + private BigDecimal normalizeGeneratedMileage(BigDecimal mileage) { + return mileage != null && mileage.compareTo(BigDecimal.valueOf(-1)) == 0 ? null : mileage; + } + private String formatMoney(BigDecimal value) { return money(value).toPlainString(); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java index 79baf7e..be33891 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java @@ -425,7 +425,7 @@ public class ShippingTemplateServiceImpl extends BaseServiceImpl latestList = list(Wrappers.lambdaQuery() .select(ShippingTemplate::getTemplateCode) .likeRight(ShippingTemplate::getTemplateCode, prefix) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 60b3984..b9df29f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -37,11 +37,13 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.pojo.entity.LoadingManage; +import org.springblade.transport.pojo.entity.ProcessConfig; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.service.ILoadingManageService; +import org.springblade.transport.service.IProcessConfigService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.TransportBusinessSupport; @@ -72,6 +74,9 @@ public class WaybillServiceImpl extends BaseServiceImpl @jakarta.annotation.Resource private ILoadingManageService loadingManageService; + @jakarta.annotation.Resource + private IProcessConfigService processConfigService; + @jakarta.annotation.Resource @org.springframework.context.annotation.Lazy private IReceivablePayableDetailService receivablePayableDetailService; @@ -93,9 +98,14 @@ public class WaybillServiceImpl extends BaseServiceImpl boolean created = Func.isEmpty(waybill.getId()); if (!created) { Waybill oldRecord = loadEditable(waybill.getId(), true); + assertNotLoaded(oldRecord); waybill.setWaybillNo(oldRecord.getWaybillNo()); + waybill.setLoadingNo(oldRecord.getLoadingNo()); waybill.setDeptId(oldRecord.getDeptId()); waybill.setDeptName(oldRecord.getDeptName()); + } else { + waybill.setLoadingNo(null); + fillProjectProcessConfig(waybill); } prepare(waybill); if (created && Func.isEmpty(waybill.getWaybillNo())) { @@ -105,6 +115,33 @@ public class WaybillServiceImpl extends BaseServiceImpl return saveOrUpdate(waybill); } + private void fillProjectProcessConfig(Waybill waybill) { + if (Func.isNotEmpty(waybill.getProcessJson()) || Func.isEmpty(waybill.getProjectId())) { + return; + } + String projectId = String.valueOf(waybill.getProjectId()); + processConfigService.list(Wrappers.lambdaQuery() + .eq(ProcessConfig::getStatus, 1) + .eq(ProcessConfig::getIsDeleted, 0) + .like(ProcessConfig::getProjectIds, projectId) + .orderByDesc(ProcessConfig::getCreateTime)) + .stream() + .filter(processConfig -> containsProjectId(processConfig.getProjectIds(), projectId)) + .map(ProcessConfig::getNodeConfigJson) + .filter(Func::isNotEmpty) + .findFirst() + .ifPresent(waybill::setProcessJson); + } + + private boolean containsProjectId(String projectIds, String projectId) { + if (Func.isEmpty(projectIds)) { + return false; + } + return List.of(projectIds.split(",")).stream() + .map(String::trim) + .anyMatch(projectId::equals); + } + @Override @Transactional(rollbackFor = Exception.class) public BusinessRemoveResultVO removeWaybill(String ids) { @@ -223,20 +260,21 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setPlanId(source.getPlanId()); target.setPlanName(source.getPlanName()); target.setMasterNo(source.getMasterNo()); - target.setLoadingNo(source.getLoadingNo()); + target.setLoadingNo(null); target.setBatchNo(source.getBatchNo()); target.setRelationNo(source.getRelationNo()); target.setCurrentProcessNode(source.getCurrentProcessNode()); target.setGoodsJson(source.getGoodsJson()); target.setCarrierJson(source.getCarrierJson()); target.setTaskInfoJson(source.getTaskInfoJson()); - target.setProcessJson(source.getProcessJson()); + target.setProcessJson(null); target.setRouteJson(source.getRouteJson()); target.setFreightJson(source.getFreightJson()); target.setAttachmentsJson(source.getAttachmentsJson()); target.setRemark(source.getRemark()); target.setBusinessStatus("pending"); target.setWaybillNo(nextCode()); + fillProjectProcessConfig(target); prepare(target); validate(target); save(target); @@ -247,6 +285,7 @@ public class WaybillServiceImpl extends BaseServiceImpl @Transactional(rollbackFor = Exception.class) public boolean changeRoute(Waybill waybill) { Waybill oldRecord = loadEditable(waybill.getId(), true); + assertNotLoaded(oldRecord); if ("completed".equals(oldRecord.getBusinessStatus()) || "cancelled".equals(oldRecord.getBusinessStatus())) { throw new ServiceException("当前运单状态不允许变更运输路线"); } @@ -265,6 +304,7 @@ public class WaybillServiceImpl extends BaseServiceImpl @Transactional(rollbackFor = Exception.class) public boolean cancel(Long id) { Waybill waybill = loadEditable(id, true); + assertNotLoaded(waybill); if ("completed".equals(waybill.getBusinessStatus()) || "cancelled".equals(waybill.getBusinessStatus())) { throw new ServiceException("当前状态不允许取消"); } @@ -276,6 +316,7 @@ public class WaybillServiceImpl extends BaseServiceImpl @Transactional(rollbackFor = Exception.class) public boolean reassign(Long id) { Waybill waybill = loadEditable(id, true); + assertNotLoaded(waybill); if (!"pending".equals(waybill.getBusinessStatus())) { throw new ServiceException("仅待执行运单允许重新派单"); } @@ -294,6 +335,7 @@ public class WaybillServiceImpl extends BaseServiceImpl boolean updated = updateById(waybill); if (updated) { receivablePayableDetailService.generateForCompletedWaybills(List.of(waybill.getId())); + loadingManageService.completeIfAllWaybillsCompleted(waybill.getLoadingNo()); } return updated; } @@ -636,7 +678,13 @@ public class WaybillServiceImpl extends BaseServiceImpl } private boolean shouldSkipDelete(Waybill waybill) { - return false; + return Func.isNotEmpty(waybill.getLoadingNo()); + } + + private void assertNotLoaded(Waybill waybill) { + if (Func.isNotEmpty(waybill.getLoadingNo())) { + throw new ServiceException("运单已关联配载单,请在配载单中修改"); + } } private void validateRoadTaskInfo(Waybill waybill) { From 35b3d1b0edd601ae90d71aaf42d81bbf5d2f168f Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 21 Aug 2026 00:17:46 +0800 Subject: [PATCH 031/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A1=B9=E7=9B=AE-?= =?UTF-8?q?=E5=90=88=E5=90=8C-=E8=BF=90=E5=8D=95-=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/ContractManageServiceImpl.java | 27 ++ .../impl/FormalSettlementServiceImpl.java | 2 +- .../impl/LoadingManageServiceImpl.java | 10 + .../impl/PreSettlementServiceImpl.java | 4 +- .../ReceivablePayableDetailServiceImpl.java | 299 ++++++++++++++---- 5 files changed, 286 insertions(+), 56 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index 21eefe7..b1e79eb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -91,6 +91,9 @@ public class ContractManageServiceImpl extends BaseServiceImpllambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getContractCategory, contractManage.getContractCategory()) + .eq(ContractManage::getPartyA, contractManage.getPartyA()) + .eq(ContractManage::getPartyB, contractManage.getPartyB()) + .ne(Func.isNotEmpty(contractManage.getId()), ContractManage::getId, contractManage.getId())); + if (count > 0) { + throw new ServiceException("相同合同类别、甲方和乙方的合同已存在,不能重复提交"); + } + } + private void validateTemporary(ContractManage contractManage) { TransportBusinessSupport.validateRequired(contractManage.getSignType(), "请选择签约类型"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 4eb319f..fa3efc5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -485,7 +485,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl selectLoadingManagePage(IPage page, LoadingManageVO loadingManage) { IPage entityPage = page(page, buildQuery(loadingManage)); @@ -268,7 +274,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl associatedWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); syncAssociatedWaybills(loadingManage, STATUS_COMPLETED, new ArrayList<>()); + if (result) { + receivablePayableDetailService.generateForCompletedWaybills(associatedWaybillIds); + } return result; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 6564d2d..4cd99aa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -1095,8 +1095,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl row.setQuantityUnit(detail.getQuantityUnit())); + rows.forEach(row -> { + if (Func.isEmpty(row.getQuantityUnit())) { + row.setQuantityUnit(detail.getQuantityUnit()); + } + }); } ReceivablePayableFeeDetailVO result = buildFeeDetail(rows); LinkedHashSet feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); @@ -463,29 +468,22 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(waybillIds)) return; for (Long waybillId : waybillIds) { Waybill waybill = waybillService.getById(waybillId); - if (waybill == null || Func.isEmpty(waybill.getContractId())) continue; - ContractManage contract = contractManageService.getById(waybill.getContractId()); - if (contract == null || !isSystemGeneration(contract)) continue; + if (waybill == null) continue; + ContractManage receivableContract = Func.isEmpty(waybill.getContractId()) + ? null : contractManageService.getById(waybill.getContractId()); try { - String planId = matchedPlanId(waybill, contract); - if (Func.isEmpty(planId)) continue; - // 自动生成只按合同计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 - List matchedFees = calculatedFees(waybill, contract, planId, true); - if (matchedFees.isEmpty()) continue; - BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); - for (String settlementType : List.of("payable", "receivable")) { - if (existsByWaybill(waybill.getId(), settlementType)) continue; - ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees, contractUnitPrice); - save(detail); - for (ReceivablePayableCargoFee fee : matchedFees) { - ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); - copy.setId(null); - copy.setDetailId(detail.getId()); - cargoFeeMapper.insert(copy); + if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory()) + && isSystemGeneration(receivableContract)) { + generateCompletedWaybillDetail(waybill, receivableContract, "receivable"); + } + if (Func.isNotEmpty(waybill.getCarrierName())) { + ContractManage payableContract = findCarrierContract(waybill); + if (payableContract != null && isSystemGeneration(payableContract)) { + generateCompletedWaybillDetail(waybill, payableContract, "payable"); } } } catch (Exception exception) { - log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, contractId:{}, failureReason:{}", + log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, receivableContractId:{}, failureReason:{}", waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception); if (exception instanceof RuntimeException runtimeException) { throw runtimeException; @@ -495,6 +493,46 @@ public class ReceivablePayableDetailServiceImpl } } + private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType) { + if (existsByWaybill(waybill.getId(), settlementType)) return; + String planId = matchedPlanId(waybill, contract); + if (Func.isEmpty(planId)) return; + // 自动生成只按合同默认计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 + List matchedFees = calculatedFees(waybill, contract, planId, true); + if (matchedFees.isEmpty()) return; + BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); + ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees, contractUnitPrice); + save(detail); + for (ReceivablePayableCargoFee fee : matchedFees) { + ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); + copy.setId(null); + copy.setDetailId(detail.getId()); + cargoFeeMapper.insert(copy); + } + } + + private ContractManage findCarrierContract(Waybill waybill) { + if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) + || Func.isEmpty(waybill.getCarrierName())) { + return null; + } + // 承运商合同甲方应与运单绑定的客户合同甲方一致,不能直接使用运单的客户名称拼接字段。 + ContractManage customerContract = contractManageService.getById(waybill.getContractId()); + if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) { + return null; + } + return contractManageService.getOne(Wrappers.lambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, waybill.getProjectId()) + .eq(ContractManage::getPartyA, customerContract.getPartyA()) + .eq(ContractManage::getPartyB, waybill.getCarrierName()) + .eq(ContractManage::getContractCategory, "承运商合同") + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) + .or().ne(ContractManage::getContractStage, "terminated")) + .orderByDesc(ContractManage::getCreateTime), false); + } + @Override @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) @@ -549,8 +587,8 @@ public class ReceivablePayableDetailServiceImpl private String matchedPlanId(Waybill waybill, ContractManage contract) { List> plans = parseList(contract.getBillingPlanJson()); if (plans.isEmpty()) return null; - Map plan = plans.stream().filter(item -> Boolean.TRUE.equals(item.get("defaultPlan"))).findFirst() - .orElse(plans.get(plans.size() - 1)); + Map plan = plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null); + if (plan == null) return null; if (!(plan.get("rules") instanceof List rules)) return null; boolean matched = rules.stream().anyMatch(value -> value instanceof Map raw && matchesRule(raw, waybill)); if (!matched) return null; @@ -559,10 +597,21 @@ public class ReceivablePayableDetailServiceImpl private boolean matchesRule(Map raw, Waybill waybill) { Object conditionValue = raw.get("matchCondition"); - if (!(conditionValue instanceof Map condition)) return true; + if (!(conditionValue instanceof Map condition) || !hasConfiguredMatchCondition(condition)) return true; return matchesLocation(condition, "origin", waybill.getDepartureAddressId(), waybill.getDepartureName(), waybill.getDepartureAddress()) && matchesLocation(condition, "destination", waybill.getArrivalAddressId(), waybill.getArrivalName(), waybill.getArrivalAddress()) - && matchesCondition(condition.get("cargoType"), waybill.getCargoType()); + && matchesCondition(condition.get("transportMode"), waybill.getTransportType()) + && matchesCargoType(condition, waybill); + } + + private boolean hasConfiguredMatchCondition(Map condition) { + return !isBlank(condition.get("origin")) + || !isBlank(condition.get("originCode")) + || !isBlank(condition.get("destination")) + || !isBlank(condition.get("destinationCode")) + || !isBlank(condition.get("transportMode")) + || !isBlank(condition.get("cargoType")) + || !isBlank(condition.get("cargoTypeCode")); } private boolean matchesLocation(Map condition, String location, Long addressId, String addressName, @@ -574,10 +623,19 @@ public class ReceivablePayableDetailServiceImpl if (!isBlank(expectedCode) && !isBlank(actualCode)) { return matchesCondition(expectedCode, actualCode); } + if (!isBlank(expectedCode) && isBlank(expectedName)) return false; return matchesCondition(expectedName, addressName) || matchesCondition(expectedName, detailAddress); } + private boolean matchesCargoType(Map condition, Waybill waybill) { + Object expectedName = condition.get("cargoType"); + Object expectedCode = condition.get("cargoTypeCode"); + if (!isBlank(expectedName)) return matchesCondition(expectedName, waybill.getCargoType()); + if (!isBlank(expectedCode)) return matchesCondition(expectedCode, waybill.getCargoType()); + return true; + } + private String resolveRegionCode(Long addressId) { if (addressId == null) return ""; try { @@ -590,6 +648,9 @@ public class ReceivablePayableDetailServiceImpl } private boolean isBlank(Object value) { + if (value instanceof Collection collection) { + return collection.isEmpty() || collection.stream().allMatch(this::isBlank); + } return value == null || String.valueOf(value).trim().isEmpty(); } @@ -641,11 +702,30 @@ public class ReceivablePayableDetailServiceImpl } private LambdaQueryWrapper buildWaybillQuery(ReceivablePayableGenerateRequest request) { + String targetSettlementType = settlementType(request.getSettlementType()); + ContractManage contract = contractManageService.getById(request.getContractId()); + validateContractSettlementType(contract, targetSettlementType); LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(Waybill::getIsDeleted, 0) - .eq(Waybill::getContractId, request.getContractId()) .eq(Waybill::getBusinessStatus, "completed"); - String targetSettlementType = settlementType(request.getSettlementType()); + if ("receivable".equals(targetSettlementType)) { + wrapper.eq(Waybill::getContractId, contract.getId()); + } else { + List customerContractIds = contractManageService.list(Wrappers.lambdaQuery() + .select(ContractManage::getId) + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, contract.getProjectId()) + .eq(ContractManage::getContractCategory, "客户合同") + .eq(ContractManage::getPartyA, contract.getPartyA())) + .stream().map(ContractManage::getId).toList(); + if (customerContractIds.isEmpty()) { + wrapper.eq(Waybill::getId, -1L); + } else { + wrapper.in(Waybill::getContractId, customerContractIds) + .eq(Waybill::getProjectId, contract.getProjectId()) + .eq(Waybill::getCarrierName, contract.getPartyB()); + } + } wrapper.notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail" + " where is_deleted = 0 and waybill_id is not null and settlement_type = '" @@ -687,9 +767,9 @@ public class ReceivablePayableDetailServiceImpl detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId()); detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName()); detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate()); - detail.setCustomerName("payable".equals(settlementType) - ? (contract == null ? waybill.getCarrierName() : contract.getPartyA()) - : (contract == null ? waybill.getCustomerName() : contract.getPartyB())); + detail.setCustomerName(contract == null + ? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName()) + : ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA())); detail.setContractId(contract == null ? waybill.getContractId() : contract.getId()); detail.setContractNo(contract == null ? null : contract.getContractNo()); detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName()); @@ -710,8 +790,7 @@ public class ReceivablePayableDetailServiceImpl detail.setOtherFeeAmount(other); detail.setTotalAmount(total); detail.setSettlementStatus("pending"); - detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, - (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); + detail.setFeeItemsJson(aggregateFeeItemsJson(fees)); return detail; } @@ -854,14 +933,20 @@ public class ReceivablePayableDetailServiceImpl if (matchOnly && !matchesRule(raw, waybill)) continue; BigDecimal amount = calculateRule(rule, waybill); if (amount == null) continue; + String feeItem = stringValue(rule, "feeItem", "费用"); + Map feeGoods = summarizeFeeGoods(rule, waybill); ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); - fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType()); + fee.setCargoName(feeGoods.getOrDefault("cargoName", waybill.getCargoName())); + fee.setCargoType(feeGoods.getOrDefault("cargoType", waybill.getCargoType())); + fee.setSpecification(feeGoods.getOrDefault("specification", waybill.getSpecification())); + fee.setModel(feeGoods.getOrDefault("model", waybill.getModel())); fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); - fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(waybill.getQuantityUnit()); + fee.setTransportQuantity(measure(rule, waybill)); + fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", waybill.getQuantityUnit())); fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); fee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); - fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount))); + fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount))); fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); result.add(fee); } @@ -894,16 +979,22 @@ public class ReceivablePayableDetailServiceImpl private BigDecimal calculateRule(Map rule, Waybill waybill) { String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", ""); BigDecimal base = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice")); - if ("按重量".equals(element) || "按吨·公里".equals(element)) { - BigDecimal minimum = decimal(rule.get("minimumBillingWeight")); - if (minimum.signum() > 0 && "按重量".equals(element)) base = base.max(minimum); - if (minimum.signum() > 0 && "按吨·公里".equals(element)) base = base.divide(money(waybill.getQuantity()).max(BigDecimal.ONE), 6, RoundingMode.HALF_UP).multiply(minimum).multiply(money(waybill.getMileage())); - } - if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP); List> ranges = ranges(rule); + boolean intervalUnitPrice = "区间单价".equals(type); + boolean intervalFlatPrice = type.contains("区间") && type.contains("一口价"); + if (!intervalUnitPrice && !intervalFlatPrice) base = applyMinimum(base, rule, waybill); + if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP); if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); - if (type.contains("区间") && type.contains("一口价")) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); - if ("区间单价".equals(type)) { BigDecimal rangeBase = base; return range(ranges, rangeBase).map(r -> decimal(r.get("unitPrice")).multiply(rangeBase)).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); } + if (intervalFlatPrice) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); + if (intervalUnitPrice) { + BigDecimal rangeBase = base; + return range(ranges, rangeBase).map(r -> { + BigDecimal minimum = decimal(r.get("minimumBillingWeight")); + if (minimum.signum() <= 0) minimum = decimal(rule.get("minimumBillingWeight")); + BigDecimal effectiveBase = applyMinimum(rangeBase, element, minimum, waybill); + return decimal(r.get("unitPrice")).multiply(effectiveBase); + }).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); + } if ("阶梯单价".equals(type)) { BigDecimal total = BigDecimal.ZERO, previous = BigDecimal.ZERO; for (Map r : ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList()) { @@ -916,6 +1007,20 @@ public class ReceivablePayableDetailServiceImpl return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); } + private BigDecimal applyMinimum(BigDecimal base, Map rule, Waybill waybill) { + return applyMinimum(base, stringValue(rule, "billingElement", ""), decimal(rule.get("minimumBillingWeight")), waybill); + } + + private BigDecimal applyMinimum(BigDecimal base, String element, BigDecimal minimum, Waybill waybill) { + if (minimum.signum() <= 0) return base; + if ("按重量".equals(element)) return base.max(minimum); + if ("按吨·公里".equals(element)) { + BigDecimal effectiveWeight = money(waybill.getQuantity()).max(minimum); + return effectiveWeight.multiply(money(waybill.getMileage())); + } + return base; + } + private boolean isFreight(ReceivablePayableCargoFee fee) { return fee.getFreightAmount() != null && fee.getFreightAmount().compareTo(BigDecimal.ZERO) > 0; } @@ -926,21 +1031,91 @@ public class ReceivablePayableDetailServiceImpl } private BigDecimal measure(Map rule, Waybill waybill) { - return switch (stringValue(rule, "billingElement", "按重量")) { - case "按体积" -> volume(waybill); + String element = stringValue(rule, "billingElement", "按重量"); + List> goods = feeGoods(rule, waybill); + return switch (element) { + case "按体积" -> goods.stream().map(this::goodsVolume).reduce(BigDecimal.ZERO, BigDecimal::add); case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE; case "按里程" -> money(waybill.getMileage()); - case "按吨·公里" -> money(waybill.getQuantity()).multiply(money(waybill.getMileage())); - case "按数量" -> money(waybill.getQuantity()); - default -> money(waybill.getQuantity()); + case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage())); + case "按数量" -> goodsQuantity(goods); + default -> goodsQuantity(goods); }; } + private List> feeGoods(Map rule, Waybill waybill) { + List> goods = parseList(waybill.getGoodsJson()); + if (goods.isEmpty()) return List.of(); + String element = stringValue(rule, "billingElement", "按重量"); + if ("按体积".equals(element)) { + return goods.stream().filter(this::isVolumeGoods).toList(); + } + if ("按重量".equals(element) || "按吨·公里".equals(element)) { + List> weighted = goods.stream().filter(item -> !isVolumeGoods(item)).toList(); + return weighted.isEmpty() ? goods : weighted; + } + return goods; + } + + private Map summarizeFeeGoods(Map rule, Waybill waybill) { + List> goods = feeGoods(rule, waybill); + if (goods.isEmpty()) return Map.of(); + Map result = new LinkedHashMap<>(); + putCommonGoodsValue(result, "cargoName", goods); + putCommonGoodsValue(result, "cargoType", goods); + putCommonGoodsValue(result, "specification", goods); + putCommonGoodsValue(result, "model", goods); + putCommonGoodsValue(result, "quantityUnit", goods); + return result; + } + + private void putCommonGoodsValue(Map result, String key, List> goods) { + List values = goods.stream().map(item -> stringValue(item, key, "")).filter(Func::isNotEmpty).distinct().toList(); + if (values.size() == 1) result.put(key, values.get(0)); + else if (!values.isEmpty()) result.put(key, String.join(",", values)); + } + + private BigDecimal goodsQuantity(List> goods) { + return goods.stream().map(item -> decimal(item.get("quantity"))).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal goodsVolume(Map goods) { + BigDecimal value = decimal(goods.get("volume")); + if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); + if (value.signum() == 0 && isVolumeGoods(goods)) value = decimal(goods.get("quantity")); + return value; + } + + private boolean isVolumeGoods(Map goods) { + if (decimal(goods.get("volume")).signum() > 0 || decimal(goods.get("cargoVolume")).signum() > 0) return true; + return isVolumeUnit(stringValue(goods, "quantityUnit", "")); + } + private BigDecimal volume(Waybill waybill) { + BigDecimal total = BigDecimal.ZERO; + for (Map goods : parseList(waybill.getGoodsJson())) { + BigDecimal value = decimal(goods.get("volume")); + if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); + if (value.signum() == 0 && isVolumeUnit(stringValue(goods, "quantityUnit"))) { + value = decimal(goods.get("quantity")); + } + total = total.add(value); + } + if (total.signum() > 0) return total; Map goods = parseMap(waybill.getGoodsJson()); BigDecimal value = decimal(goods.get("volume")); if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); - return value; + if (value.signum() == 0 && isVolumeUnit(stringValue(goods, "quantityUnit"))) { + value = decimal(goods.get("quantity")); + } + if (value.signum() > 0) return value; + return isVolumeUnit(waybill.getQuantityUnit()) ? money(waybill.getQuantity()) : BigDecimal.ZERO; + } + + private boolean isVolumeUnit(String unit) { + String normalized = String.valueOf(unit == null ? "" : unit).trim().toLowerCase(); + return normalized.equals("方") || normalized.contains("立方") + || normalized.equals("m3") || normalized.equals("m³") || normalized.equals("m^3"); } private List> ranges(Map rule) { @@ -1062,6 +1237,13 @@ public class ReceivablePayableDetailServiceImpl updateById(detail); } + private String aggregateFeeItemsJson(List fees) { + Map feeItems = new LinkedHashMap<>(); + fees.forEach(fee -> parseMap(fee.getFeeItemsJson()).forEach((name, value) -> + feeItems.merge(name, decimal(value), BigDecimal::add))); + return JsonUtil.toJson(feeItems); + } + private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) { if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) { rebuildMasterOrderDetailFee(detail, billingPlanId); @@ -1081,7 +1263,7 @@ public class ReceivablePayableDetailServiceImpl detail.setOtherFeeAmount(total.subtract(freight)); detail.setTotalAmount(total); detail.setUnitPrice(resolveContractUnitPrice(fees)); - detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); + detail.setFeeItemsJson(aggregateFeeItemsJson(fees)); updateById(detail); } @@ -1112,8 +1294,7 @@ public class ReceivablePayableDetailServiceImpl detail.setOtherFeeAmount(total.subtract(freight)); detail.setTotalAmount(total); detail.setUnitPrice(resolveContractUnitPrice(fees)); - detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, - (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); + detail.setFeeItemsJson(aggregateFeeItemsJson(fees)); updateById(detail); } @@ -1198,12 +1379,24 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(request.getBillingPlanId())) { throw new ServiceException("请选择计费方案"); } - settlementType(request.getSettlementType()); + String targetSettlementType = settlementType(request.getSettlementType()); + validateContractSettlementType(contractManageService.getById(request.getContractId()), targetSettlementType); if (requireWaybill && Func.isEmpty(request.getWaybillIds())) { throw new ServiceException("请选择需要生成费用的运单"); } } + private void validateContractSettlementType(ContractManage contract, String settlementType) { + if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) { + throw new ServiceException("合同不存在或已删除"); + } + String expectedCategory = "payable".equals(settlementType) ? "承运商合同" : "客户合同"; + if (!expectedCategory.equals(contract.getContractCategory())) { + throw new ServiceException(("payable".equals(settlementType) ? "应付" : "应收") + + "费用只能使用" + expectedCategory); + } + } + private Map waybillMap(Waybill waybill) { Map map = new LinkedHashMap<>(); map.put("id", waybill.getId()); From 762d6e4768873fe1b8035aebab8af22701fde8c2 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 21 Aug 2026 12:07:21 +0800 Subject: [PATCH 032/114] fix bug --- .../ReceivablePayableAdjustFeeRequest.java | 3 + .../entity/ReceivablePayableCargoFee.java | 3 + .../ReceivablePayableDetailController.java | 12 +- .../IReceivablePayableDetailService.java | 4 + .../ReceivablePayableDetailServiceImpl.java | 342 ++++++++++++++++-- ...ade_receivable_payable_detail_20260812.sql | 1 + 6 files changed, 324 insertions(+), 41 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java index e34efbc..25fa656 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java @@ -70,5 +70,8 @@ public class ReceivablePayableAdjustFeeRequest implements Serializable { @Schema(description = "备注") private String remark; + + @Schema(description = "变更原因") + private String changeReason; } } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java index 151fd87..9c626d6 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java @@ -105,4 +105,7 @@ public class ReceivablePayableCargoFee extends TenantEntity { @Schema(description = "备注") private String remark; + @Schema(description = "变更原因") + private String changeReason; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java index f72c499..9790e3e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java @@ -36,10 +36,12 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; +import org.springblade.transport.pojo.dto.ReceivablePayableFeeCalculateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; +import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; import org.springblade.transport.service.IReceivablePayableDetailService; @@ -119,8 +121,16 @@ public class ReceivablePayableDetailController extends BladeController { return R.success("保存成功"); } - @GetMapping("/transfer-candidates") + @PostMapping("/calculate-adjusted-fee") @ApiOperationSupport(order = 7) + @Operation(summary = "调整费用试算") + public R calculateAdjustedFee( + @RequestBody ReceivablePayableFeeCalculateRequest request) { + return R.data(detailService.calculateAdjustedFee(request)); + } + + @GetMapping("/transfer-candidates") + @ApiOperationSupport(order = 8) @Operation(summary = "转结算候选明细") public R>> transferCandidates(Query query, @RequestParam(required = false) String contractName, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index 1dc0ab8..0cae341 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -25,12 +25,14 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; +import org.springblade.transport.pojo.dto.ReceivablePayableFeeCalculateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; import org.springblade.transport.pojo.entity.MasterOrder; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; +import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; @@ -58,6 +60,8 @@ public interface IReceivablePayableDetailService extends BaseService> transferCandidates(IPage page, String contractName, String batchNo, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 44dd80f..8a0135d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -38,6 +38,7 @@ import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.MasterOrderMapper; import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; +import org.springblade.transport.pojo.dto.ReceivablePayableFeeCalculateRequest; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; @@ -74,6 +75,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -125,12 +127,18 @@ public class ReceivablePayableDetailServiceImpl @Override public IPage selectPage(IPage page, ReceivablePayableDetailVO query) { - return ReceivablePayableDetailWrapper.build().pageVO(page(page, buildQuery(query))); + IPage result = ReceivablePayableDetailWrapper.build() + .pageVO(page(page, buildQuery(query))); + fillCargoNames(result.getRecords()); + return result; } @Override public List selectList(ReceivablePayableDetailVO query) { - return ReceivablePayableDetailWrapper.build().listVO(list(buildQuery(query))); + List result = ReceivablePayableDetailWrapper.build() + .listVO(list(buildQuery(query))); + fillCargoNames(result); + return result; } @Override @@ -257,6 +265,9 @@ public class ReceivablePayableDetailServiceImpl if (adjusted.getRemark() != null && adjusted.getRemark().length() > 200) { throw new ServiceException("备注不能超过200个字"); } + if (adjusted.getChangeReason() != null && adjusted.getChangeReason().length() > 300) { + throw new ServiceException("变更原因不能超过300个字"); + } if (Boolean.TRUE.equals(adjusted.getManualFee())) { if (Func.isEmpty(adjusted.getFeeItemName())) { throw new ServiceException("请填写手工费用项目"); @@ -290,6 +301,7 @@ public class ReceivablePayableDetailServiceImpl existing.setAdjustAmount(signedAmount.subtract(money(existing.getOriginalAmount()))); existing.setAfterAmount(signedAmount); existing.setRemark(adjusted.getRemark()); + existing.setChangeReason(adjusted.getChangeReason()); if (newManualRow) { cargoFeeMapper.insert(existing); allRows.add(existing); @@ -316,24 +328,40 @@ public class ReceivablePayableDetailServiceImpl feeItems.put(name, money(amount)); }); } - appendChange(changes, "计费数量", existing.getTransportQuantity(), adjusted.getTransportQuantity()); - appendChange(changes, "里程", existing.getMileage(), adjusted.getMileage()); - appendChange(changes, "运输费", existing.getFreightAmount(), adjusted.getFreightAmount()); + BigDecimal transportQuantity = money(adjusted.getTransportQuantity()); + BigDecimal mileage = money(adjusted.getMileage()); + BigDecimal freightAmount = money(adjusted.getFreightAmount()); + Map effectiveFeeItems = feeItems; + boolean ruleInputChanged = money(existing.getTransportQuantity()).compareTo(transportQuantity) != 0 + || money(existing.getMileage()).compareTo(mileage) != 0; + if (ruleInputChanged) { + AdjustedFeeCalculation calculation = calculateAdjustedFee(detail, existing, transportQuantity, + mileage, freightAmount, effectiveFeeItems); + freightAmount = calculation.freightAmount(); + effectiveFeeItems = calculation.feeItems(); + } + appendChange(changes, "计费数量", existing.getTransportQuantity(), transportQuantity); + appendChange(changes, "里程", existing.getMileage(), mileage); + appendChange(changes, "运输费", existing.getFreightAmount(), freightAmount); if (!Objects.equals(existing.getRemark(), adjusted.getRemark())) { changes.add("【备注】从[" + Objects.toString(existing.getRemark(), "") + "]调整为[" + Objects.toString(adjusted.getRemark(), "") + "]"); } + if (!Objects.equals(existing.getChangeReason(), adjusted.getChangeReason())) { + changes.add("【变更原因】从[" + Objects.toString(existing.getChangeReason(), "") + "]调整为[" + + Objects.toString(adjusted.getChangeReason(), "") + "]"); + } Map oldFeeItems = parseMap(existing.getFeeItemsJson()); for (String name : allowedFeeItems) { - appendChange(changes, name, decimal(oldFeeItems.get(name)), money(feeItems.get(name))); + appendChange(changes, name, decimal(oldFeeItems.get(name)), money(effectiveFeeItems.get(name))); } - BigDecimal freightAmount = money(adjusted.getFreightAmount()); - BigDecimal afterAmount = adjustedAfterAmount(freightAmount, feeItems); - existing.setTransportQuantity(money(adjusted.getTransportQuantity())); - existing.setMileage(money(adjusted.getMileage())); + BigDecimal afterAmount = adjustedAfterAmount(freightAmount, effectiveFeeItems); + existing.setTransportQuantity(transportQuantity); + existing.setMileage(mileage); existing.setFreightAmount(freightAmount); - existing.setFeeItemsJson(JsonUtil.toJson(feeItems)); + existing.setFeeItemsJson(JsonUtil.toJson(effectiveFeeItems)); existing.setRemark(adjusted.getRemark()); + existing.setChangeReason(adjusted.getChangeReason()); existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount()))); existing.setAfterAmount(afterAmount); cargoFeeMapper.updateById(existing); @@ -347,6 +375,43 @@ public class ReceivablePayableDetailServiceImpl refreshAdjustedDetail(detail, allRows); } + @Override + public ReceivablePayableCargoFeeVO calculateAdjustedFee(ReceivablePayableFeeCalculateRequest request) { + if (request == null || request.getDetailId() == null || request.getFeeId() == null) { + throw new ServiceException("费用调整试算数据不能为空"); + } + ReceivablePayableDetail detail = getExisting(request.getDetailId()); + if (!"pending".equals(detail.getSettlementStatus())) { + throw new ServiceException("仅待结算明细允许调整试算"); + } + ReceivablePayableCargoFee fee = cargoFeeMapper.selectOne( + Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getId, request.getFeeId()) + .eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0)); + if (fee == null) { + throw new ServiceException("费用明细不存在"); + } + validateNonNegative(request.getTransportQuantity(), "运输量"); + validateNonNegative(request.getMileage(), "里程"); + AdjustedFeeCalculation calculation = calculateAdjustedFee(detail, fee, + money(request.getTransportQuantity()), money(request.getMileage()), + money(request.getFreightAmount()), normalizeFeeItems(request.getFeeItems())); + ReceivablePayableCargoFeeVO result = Objects.requireNonNull( + BeanUtil.copyProperties(fee, ReceivablePayableCargoFeeVO.class)); + result.setTransportQuantity(request.getTransportQuantity()); + result.setMileage(request.getMileage()); + result.setFreightAmount(calculation.freightAmount()); + result.setFeeItems(new LinkedHashMap<>(calculation.feeItems())); + BigDecimal afterAmount = adjustedAfterAmount(calculation.freightAmount(), calculation.feeItems()); + result.setAdjustAmount(afterAmount.subtract(money(fee.getOriginalAmount()))); + result.setAfterAmount(afterAmount); + result.setOriginalAmountText(formatMoney(fee.getOriginalAmount())); + result.setAdjustAmountText(formatMoney(result.getAdjustAmount())); + result.setAfterAmountText(formatMoney(afterAmount)); + return result; + } + @Override @Transactional(rollbackFor = Exception.class) public void transferSettlement(ReceivablePayableTransferRequest request) { @@ -412,6 +477,7 @@ public class ReceivablePayableDetailServiceImpl .or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")); IPage detailPage = ReceivablePayableDetailWrapper.build() .pageVO(page(new Page<>(page.getCurrent(), page.getSize()), wrapper)); + fillCargoNames(detailPage.getRecords()); Page> result = new Page<>(detailPage.getCurrent(), detailPage.getSize(), detailPage.getTotal()); result.setRecords(detailPage.getRecords().stream().map(this::candidateMap).toList()); return result; @@ -431,11 +497,12 @@ public class ReceivablePayableDetailServiceImpl validateGenerateRequest(request, true); List waybills = waybillService.list(buildWaybillQuery(request)); ContractManage contract = contractManageService.getById(request.getContractId()); - List fees = waybills.stream() - .skip((page.getCurrent() - 1) * page.getSize()).limit(page.getSize()) + List allFees = waybills.stream() .flatMap(waybill -> calculatedFees(waybill, contract, request.getBillingPlanId()).stream()).toList(); + List fees = allFees.stream() + .skip((page.getCurrent() - 1) * page.getSize()).limit(page.getSize()).toList(); ReceivablePayableFeeDetailVO vo = buildFeeDetail(fees); - vo.setTotal((long) waybills.size()); + vo.setTotal((long) allFees.size()); return vo; } @@ -447,6 +514,8 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(waybills)) { throw new ServiceException("没有可生成费用的运单"); } + Long currentUserId = AuthUtil.getUserId(); + Date generateTime = new Date(); ContractManage contract = contractManageService.getById(request.getContractId()); for (Waybill waybill : waybills) { if (existsByWaybill(waybill.getId(), settlementType(request.getSettlementType()))) { @@ -456,6 +525,7 @@ public class ReceivablePayableDetailServiceImpl save(detail); calculatedFees(waybill, contract, request.getBillingPlanId()).forEach(fee -> { fee.setDetailId(detail.getId()); + fillGeneratedAuditFields(fee, currentUserId, generateTime); cargoFeeMapper.insert(fee); }); } @@ -466,6 +536,8 @@ public class ReceivablePayableDetailServiceImpl rollbackFor = Exception.class) public void generateForCompletedWaybills(List waybillIds) { if (Func.isEmpty(waybillIds)) return; + Long currentUserId = AuthUtil.getUserId(); + Date generateTime = new Date(); for (Long waybillId : waybillIds) { Waybill waybill = waybillService.getById(waybillId); if (waybill == null) continue; @@ -474,12 +546,12 @@ public class ReceivablePayableDetailServiceImpl try { if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory()) && isSystemGeneration(receivableContract)) { - generateCompletedWaybillDetail(waybill, receivableContract, "receivable"); + generateCompletedWaybillDetail(waybill, receivableContract, "receivable", currentUserId, generateTime); } if (Func.isNotEmpty(waybill.getCarrierName())) { ContractManage payableContract = findCarrierContract(waybill); if (payableContract != null && isSystemGeneration(payableContract)) { - generateCompletedWaybillDetail(waybill, payableContract, "payable"); + generateCompletedWaybillDetail(waybill, payableContract, "payable", currentUserId, generateTime); } } } catch (Exception exception) { @@ -493,7 +565,8 @@ public class ReceivablePayableDetailServiceImpl } } - private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType) { + private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType, + Long currentUserId, Date generateTime) { if (existsByWaybill(waybill.getId(), settlementType)) return; String planId = matchedPlanId(waybill, contract); if (Func.isEmpty(planId)) return; @@ -507,6 +580,7 @@ public class ReceivablePayableDetailServiceImpl ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); copy.setId(null); copy.setDetailId(detail.getId()); + fillGeneratedAuditFields(copy, currentUserId, generateTime); cargoFeeMapper.insert(copy); } } @@ -542,6 +616,8 @@ public class ReceivablePayableDetailServiceImpl } ContractManage contract = contractManageService.getById(masterOrder.getContractId()); if (contract == null || !isSystemGeneration(contract)) return; + Long currentUserId = AuthUtil.getUserId(); + Date generateTime = new Date(); try { List masterGoods = masterOrderGoods(masterOrder); if (masterGoods.isEmpty()) return; @@ -564,6 +640,7 @@ public class ReceivablePayableDetailServiceImpl copy.setId(null); copy.setDetailId(detail.getId()); copy.setWaybillId(null); + fillGeneratedAuditFields(copy, currentUserId, generateTime); cargoFeeMapper.insert(copy); } } @@ -728,8 +805,8 @@ public class ReceivablePayableDetailServiceImpl } wrapper.notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail" - + " where is_deleted = 0 and waybill_id is not null and settlement_type = '" - + targetSettlementType + "'"); + + " where is_deleted = 0 and waybill_id is not null" + + " and settlement_type in ('receivable', 'payable')"); if (Func.isNotEmpty(request.getBatchNo())) { wrapper.like(Waybill::getBatchNo, request.getBatchNo()); } @@ -923,34 +1000,87 @@ public class ReceivablePayableDetailServiceImpl private List calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) { List> plans = parseList(contract == null ? null : contract.getBillingPlanJson()); Map plan = resolveBillingPlan(plans, planId); - if (plan == null || !(plan.get("rules") instanceof List)) return matchOnly ? List.of() : List.of(buildCargoFee(null, waybill)); + return calculatedFees(waybill, plan, matchOnly); + } + + private List calculatedFees(Waybill waybill, Map plan, + boolean matchOnly) { + if (plan == null || !(plan.get("rules") instanceof List)) { + return matchOnly ? List.of() : buildCargoFees(waybill); + } List result = new ArrayList<>(); int line = 1; for (Object value : (List) plan.get("rules")) { if (!(value instanceof Map raw)) continue; Map rule = new LinkedHashMap<>(); raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); - if (matchOnly && !matchesRule(raw, waybill)) continue; - BigDecimal amount = calculateRule(rule, waybill); - if (amount == null) continue; - String feeItem = stringValue(rule, "feeItem", "费用"); - Map feeGoods = summarizeFeeGoods(rule, waybill); - ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); - fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); - fee.setCargoName(feeGoods.getOrDefault("cargoName", waybill.getCargoName())); - fee.setCargoType(feeGoods.getOrDefault("cargoType", waybill.getCargoType())); - fee.setSpecification(feeGoods.getOrDefault("specification", waybill.getSpecification())); - fee.setModel(feeGoods.getOrDefault("model", waybill.getModel())); - fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); - fee.setTransportQuantity(measure(rule, waybill)); - fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", waybill.getQuantityUnit())); - fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); - fee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); - fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount))); - fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); - result.add(fee); + for (Waybill feeWaybill : feeWaybills(rule, waybill)) { + if (matchOnly && !matchesRule(raw, feeWaybill)) continue; + BigDecimal amount = calculateRule(rule, feeWaybill); + if (amount == null) continue; + String feeItem = stringValue(rule, "feeItem", "费用"); + Map feeGoods = summarizeFeeGoods(rule, feeWaybill); + ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); + fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); + fee.setCargoName(feeGoods.getOrDefault("cargoName", feeWaybill.getCargoName())); + fee.setCargoType(feeGoods.getOrDefault("cargoType", feeWaybill.getCargoType())); + fee.setSpecification(feeGoods.getOrDefault("specification", feeWaybill.getSpecification())); + fee.setModel(feeGoods.getOrDefault("model", feeWaybill.getModel())); + fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); + fee.setTransportQuantity(measure(rule, feeWaybill)); + fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", feeWaybill.getQuantityUnit())); + fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); + fee.setMileage(normalizeGeneratedMileage(feeWaybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); + fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount))); + fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", feeWaybill.getRemark())); + result.add(fee); + } } - return result.isEmpty() && !matchOnly ? List.of(buildCargoFee(null, waybill)) : result; + return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result; + } + + private List feeWaybills(Map rule, Waybill waybill) { + String element = stringValue(rule, "billingElement", "按重量"); + if (!List.of("按重量", "按体积", "按吨·公里", "按数量").contains(element)) { + return List.of(waybill); + } + List goodsWaybills = goodsWaybills(waybill); + return goodsWaybills.isEmpty() ? List.of(waybill) : goodsWaybills; + } + + private List goodsWaybills(Waybill waybill) { + return parseList(waybill.getGoodsJson()).stream().map(goods -> { + Waybill goodsWaybill = Objects.requireNonNull(BeanUtil.copyProperties(waybill, Waybill.class)); + goodsWaybill.setCargoName(stringValue(goods, "cargoName", waybill.getCargoName())); + goodsWaybill.setCargoType(stringValue(goods, "cargoType", waybill.getCargoType())); + goodsWaybill.setSpecification(stringValue(goods, "specification", waybill.getSpecification())); + goodsWaybill.setModel(stringValue(goods, "model", waybill.getModel())); + goodsWaybill.setQuantity(decimal(goods.get("quantity"))); + goodsWaybill.setQuantityUnit(stringValue(goods, "quantityUnit", waybill.getQuantityUnit())); + goodsWaybill.setUnitPrice(goods.get("unitPrice") == null + ? waybill.getUnitPrice() : decimal(goods.get("unitPrice"))); + goodsWaybill.setPriceUnit(stringValue(goods, "priceUnit", waybill.getPriceUnit())); + goodsWaybill.setGoodsJson(JsonUtil.toJson(List.of(goods))); + return goodsWaybill; + }).toList(); + } + + private List buildCargoFees(Waybill waybill) { + List goodsWaybills = goodsWaybills(waybill); + if (goodsWaybills.isEmpty()) return List.of(buildCargoFee(null, waybill)); + List fees = new ArrayList<>(); + for (int index = 0; index < goodsWaybills.size(); index++) { + Waybill goodsWaybill = goodsWaybills.get(index); + if (index > 0) { + goodsWaybill.setFreightJson(null); + goodsWaybill.setOtherFeeTotal(BigDecimal.ZERO); + } + ReceivablePayableCargoFee fee = buildCargoFee(null, goodsWaybill); + fee.setWaybillId(waybill.getId()); + fee.setLineNo(String.format("%04d", index + 1)); + fees.add(fee); + } + return fees; } private Map resolveBillingPlan(List> plans, String planId) { @@ -1198,6 +1328,104 @@ public class ReceivablePayableDetailServiceImpl } } + private Map normalizeFeeItems(Map feeItems) { + Map result = new LinkedHashMap<>(); + if (feeItems == null) return result; + feeItems.forEach((name, amount) -> { + validateNonNegative(amount, name); + result.put(name, money(amount)); + }); + return result; + } + + private AdjustedFeeCalculation calculateAdjustedFee(ReceivablePayableDetail detail, + ReceivablePayableCargoFee fee, BigDecimal transportQuantity, + BigDecimal mileage, BigDecimal freightAmount, + Map feeItems) { + if ("手工调整".equals(fee.getBillingFactor())) { + throw new ServiceException("手工费用不支持按合同计费规则试算"); + } + ContractManage contract = contractManageService.getById(detail.getContractId()); + if (contract == null) { + throw new ServiceException("关联合同不存在"); + } + Waybill adjustedWaybill = adjustedWaybill(detail, fee, transportQuantity, mileage); + List> rules = matchingAdjustedRules(contract, fee, adjustedWaybill); + if (rules.isEmpty()) { + throw new ServiceException("未找到费用明细对应的合同计费规则,请先更新费用"); + } + Map> results = new LinkedHashMap<>(); + for (Map rule : rules) { + BigDecimal amount = calculateRule(rule, adjustedWaybill); + if (amount != null) results.putIfAbsent(money(amount), rule); + } + if (results.size() != 1) { + throw new ServiceException("费用明细对应多个计费结果,无法唯一试算,请先更新费用"); + } + Map.Entry> calculated = results.entrySet().iterator().next(); + BigDecimal amount = calculated.getKey(); + Map rule = calculated.getValue(); + String feeItem = stringValue(rule, "feeItem", "费用"); + Map calculatedFeeItems = new LinkedHashMap<>(feeItems); + calculatedFeeItems.put(feeItem, amount); + BigDecimal calculatedFreight = isFreightRule(rule) ? amount : freightAmount; + return new AdjustedFeeCalculation(calculatedFreight, calculatedFeeItems); + } + + private Waybill adjustedWaybill(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee, + BigDecimal transportQuantity, BigDecimal mileage) { + Waybill source = detail.getWaybillId() == null ? null : waybillService.getById(detail.getWaybillId()); + Waybill waybill = source == null ? new Waybill() + : Objects.requireNonNull(BeanUtil.copyProperties(source, Waybill.class)); + waybill.setQuantity(transportQuantity); + waybill.setMileage(mileage); + waybill.setCargoName(fee.getCargoName()); + waybill.setCargoType(fee.getCargoType()); + waybill.setSpecification(fee.getSpecification()); + waybill.setModel(fee.getModel()); + waybill.setQuantityUnit(fee.getQuantityUnit()); + waybill.setTransportType(detail.getTransportType()); + Map goods = new LinkedHashMap<>(); + goods.put("cargoName", fee.getCargoName()); + goods.put("cargoType", fee.getCargoType()); + goods.put("specification", fee.getSpecification()); + goods.put("model", fee.getModel()); + goods.put("quantity", transportQuantity); + goods.put("quantityUnit", fee.getQuantityUnit()); + if ("按体积".equals(fee.getBillingFactor())) { + goods.put("volume", transportQuantity); + } + waybill.setGoodsJson(JsonUtil.toJson(List.of(goods))); + return waybill; + } + + private List> matchingAdjustedRules(ContractManage contract, + ReceivablePayableCargoFee fee, Waybill waybill) { + Set feeItemNames = parseMap(fee.getFeeItemsJson()).keySet(); + List> candidates = new ArrayList<>(); + for (Map plan : parseList(contract.getBillingPlanJson())) { + if (!(plan.get("rules") instanceof List rules)) continue; + for (Object value : rules) { + if (!(value instanceof Map raw)) continue; + Map rule = new LinkedHashMap<>(); + raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); + if (!Objects.equals(stringValue(rule, "billingElement"), fee.getBillingFactor()) + || !Objects.equals(stringValue(rule, "billingType"), fee.getBillingType()) + || !feeItemNames.contains(stringValue(rule, "feeItem"))) continue; + if (Func.isNotEmpty(fee.getPriceUnit()) + && !Objects.equals(stringValue(rule, "billingUnit"), fee.getPriceUnit())) continue; + candidates.add(rule); + } + } + List> unitPriceMatched = candidates.stream() + .filter(rule -> decimal(rule.get("unitPrice")).compareTo(money(fee.getUnitPrice())) == 0) + .toList(); + if (!unitPriceMatched.isEmpty()) candidates = unitPriceMatched; + List> conditionMatched = candidates.stream() + .filter(rule -> matchesRule(rule, waybill)).toList(); + return conditionMatched.isEmpty() ? candidates : conditionMatched; + } + private BigDecimal adjustedAfterAmount(BigDecimal freightAmount, Map feeItems) { BigDecimal feeItemTotal = feeItems.values().stream().map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); @@ -1208,6 +1436,9 @@ public class ReceivablePayableDetailServiceImpl return name != null && (name.contains("运费") || name.contains("运输费")); } + private record AdjustedFeeCalculation(BigDecimal freightAmount, Map feeItems) { + } + private void appendChange(List changes, String field, BigDecimal before, BigDecimal after) { BigDecimal oldValue = money(before); BigDecimal newValue = money(after); @@ -1331,6 +1562,37 @@ public class ReceivablePayableDetailServiceImpl changeRecordMapper.insert(record); } + private void fillGeneratedAuditFields(ReceivablePayableCargoFee fee, Long currentUserId, + Date generateTime) { + fee.setUpdateUser(currentUserId); + fee.setUpdateTime(generateTime); + } + + private void fillCargoNames(List details) { + if (Func.isEmpty(details)) return; + List detailIds = details.stream().map(ReceivablePayableDetailVO::getId) + .filter(Objects::nonNull).distinct().toList(); + if (detailIds.isEmpty()) return; + List fees = cargoFeeMapper.selectList( + Wrappers.lambdaQuery() + .select(ReceivablePayableCargoFee::getDetailId, ReceivablePayableCargoFee::getCargoName) + .in(ReceivablePayableCargoFee::getDetailId, detailIds) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .orderByAsc(ReceivablePayableCargoFee::getDetailId) + .orderByAsc(ReceivablePayableCargoFee::getLineNo)); + Map> cargoNames = new LinkedHashMap<>(); + for (ReceivablePayableCargoFee fee : fees) { + if (Func.isEmpty(fee.getCargoName())) continue; + cargoNames.computeIfAbsent(fee.getDetailId(), key -> new LinkedHashSet<>()).add(fee.getCargoName()); + } + details.forEach(detail -> { + Set names = cargoNames.get(detail.getId()); + if (Func.isNotEmpty(names)) { + detail.setCargoName(String.join(",", names)); + } + }); + } + private void applyManualAdjustment(ReceivablePayableDetail detail, BigDecimal amount, String feeItem, String reason) { ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); fee.setDetailId(detail.getId()); fee.setWaybillId(detail.getWaybillId()); fee.setLineNo("ADJ-" + System.currentTimeMillis()); diff --git a/doc/sql/transport/blade_receivable_payable_detail_20260812.sql b/doc/sql/transport/blade_receivable_payable_detail_20260812.sql index 4e8a6c0..b9d639e 100644 --- a/doc/sql/transport/blade_receivable_payable_detail_20260812.sql +++ b/doc/sql/transport/blade_receivable_payable_detail_20260812.sql @@ -81,6 +81,7 @@ CREATE TABLE IF NOT EXISTS `blade_receivable_payable_cargo_fee` ( `adjust_amount` decimal(18,2) DEFAULT NULL COMMENT '调整金额', `after_amount` decimal(18,2) DEFAULT NULL COMMENT '调整后总金额', `remark` varchar(200) DEFAULT NULL COMMENT '备注', + `change_reason` varchar(300) DEFAULT NULL COMMENT '变更原因', PRIMARY KEY (`id`) USING BTREE, KEY `idx_receivable_payable_cargo_detail` (`detail_id`) USING BTREE, KEY `idx_receivable_payable_cargo_waybill` (`waybill_id`) USING BTREE From bd2c53fc1569c3af366fcdb968117141612126c4 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 21 Aug 2026 12:41:34 +0800 Subject: [PATCH 033/114] fix bug --- .../ReceivablePayableFeeCalculateRequest.java | 66 +++++++++++++++++++ ...yable_cargo_fee_change_reason_20260821.sql | 27 ++++++++ 2 files changed, 93 insertions(+) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableFeeCalculateRequest.java create mode 100644 doc/sql/transport/blade_receivable_payable_cargo_fee_change_reason_20260821.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableFeeCalculateRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableFeeCalculateRequest.java new file mode 100644 index 0000000..640ac83 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableFeeCalculateRequest.java @@ -0,0 +1,66 @@ +/** + * 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.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Map; + +/** + * 应收应付费用调整试算请求 + * + * @author Chill + */ +@Data +@Schema(description = "应收应付费用调整试算请求") +public class ReceivablePayableFeeCalculateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "应收应付明细ID") + private Long detailId; + + @Schema(description = "费用行ID") + private Long feeId; + + @Schema(description = "运输量") + private BigDecimal transportQuantity; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "运输费") + private BigDecimal freightAmount; + + @Schema(description = "动态费用项目") + private Map feeItems; + +} diff --git a/doc/sql/transport/blade_receivable_payable_cargo_fee_change_reason_20260821.sql b/doc/sql/transport/blade_receivable_payable_cargo_fee_change_reason_20260821.sql new file mode 100644 index 0000000..4642556 --- /dev/null +++ b/doc/sql/transport/blade_receivable_payable_cargo_fee_change_reason_20260821.sql @@ -0,0 +1,27 @@ +-- MySQL 5.7+ 兼容:为应收应付货物费用明细增加变更原因。 +-- 请在 transport 数据库执行。本脚本使用 information_schema 判断,可重复执行。 + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS `upgrade_receivable_payable_cargo_fee_change_reason_20260821`$$ +CREATE PROCEDURE `upgrade_receivable_payable_cargo_fee_change_reason_20260821`() +BEGIN + DECLARE db_name varchar(128); + SET db_name = DATABASE(); + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = db_name + AND table_name = 'blade_receivable_payable_cargo_fee' + AND column_name = 'change_reason' + ) THEN + ALTER TABLE `blade_receivable_payable_cargo_fee` + ADD COLUMN `change_reason` varchar(300) DEFAULT NULL COMMENT '变更原因' AFTER `remark`; + END IF; +END$$ + +CALL `upgrade_receivable_payable_cargo_fee_change_reason_20260821`()$$ +DROP PROCEDURE `upgrade_receivable_payable_cargo_fee_change_reason_20260821`$$ + +DELIMITER ; From 7683ad804a19f529de41af49304c460470e1ba62 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 21 Aug 2026 14:30:59 +0800 Subject: [PATCH 034/114] fix bug --- .../transport/pojo/entity/LoadingManage.java | 3 + .../transport/pojo/entity/Waybill.java | 3 + .../pojo/vo/LoadingCarrierContractVO.java | 58 +++ .../pojo/vo/MasterOrderCarrierVO.java | 37 ++ .../controller/LoadingManageController.java | 9 + .../controller/MasterOrderController.java | 4 + .../service/ILoadingManageService.java | 3 + .../service/IMasterOrderService.java | 2 + .../IReceivablePayableDetailService.java | 5 +- .../impl/ContractManageServiceImpl.java | 5 +- .../impl/LoadingManageServiceImpl.java | 139 ++++++- .../service/impl/MasterOrderServiceImpl.java | 62 ++- .../ReceivablePayableDetailServiceImpl.java | 352 ++++++++++++------ .../service/impl/WaybillServiceImpl.java | 1 + .../blade_loading_manage_20260804.sql | 2 + ...ading_manage_carrier_contract_20260821.sql | 3 + doc/sql/transport/blade_tms_business.sql | 4 + ...lade_waybill_carrier_contract_20260821.sql | 3 + 18 files changed, 576 insertions(+), 119 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java create mode 100644 doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql create mode 100644 doc/sql/transport/blade_waybill_carrier_contract_20260821.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java index 2488c5f..6393fc0 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java @@ -80,6 +80,9 @@ public class LoadingManage extends TenantEntity { @Schema(description = "承运商") private String carrierName; + @Schema(description = "承运商合同ID") + private Long carrierContractId; + @Schema(description = "发货地址") private String departureAddress; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index cbd2b07..e712ce0 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -127,6 +127,9 @@ public class Waybill extends TenantEntity { @Schema(description = "承运商名称") private String carrierName; + @Schema(description = "承运商合同ID") + private Long carrierContractId; + @Schema(description = "司机ID") private Long driverId; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java new file mode 100644 index 0000000..22a40b8 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java @@ -0,0 +1,58 @@ +/** + * 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.transport.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; + +/** + * 配载可选承运商合同视图类 + * + * @author Chill + */ +@Data +@Schema(description = "配载可选承运商合同") +public class LoadingCarrierContractVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "承运商合同ID") + private Long id; + + @Schema(description = "承运商合同名称") + private String contractName; + + @Schema(description = "承运商名称") + private String carrierName; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java new file mode 100644 index 0000000..1ce202d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java @@ -0,0 +1,37 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.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; + +/** + * 总单调度可选承运商视图类 + * + * @author Chill + */ +@Data +@Schema(description = "总单调度可选承运商") +public class MasterOrderCarrierVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "承运商合同ID") + private Long contractId; + + @Schema(description = "承运商合同名称") + private String contractName; + + @Schema(description = "承运商名称") + private String carrierName; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java index 939ac35..39b5263 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java @@ -21,6 +21,7 @@ import org.springblade.core.tool.utils.DateUtil; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.service.ILoadingManageService; import org.springframework.web.bind.annotation.GetMapping; @@ -53,6 +54,14 @@ public class LoadingManageController extends BladeController { return R.data(loadingManageService.detail(id)); } + @GetMapping("/carrier-contracts") + @ApiOperationSupport(order = 13) + @Operation(summary = "可选承运商合同", description = "根据运单ID集合查询共同可用的承运商合同") + public R> carrierContracts( + @Parameter(description = "运单ID集合", required = true) @RequestParam List waybillIds) { + return R.data(loadingManageService.carrierContracts(waybillIds)); + } + @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页", description = "传入loadingManage") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java index 9b5151c..bfeb237 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java @@ -15,6 +15,7 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest; import org.springblade.transport.excel.MasterOrderWaybillExcel; +import org.springblade.transport.pojo.vo.MasterOrderCarrierVO; import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.service.IMasterOrderService; import org.springframework.web.bind.annotation.GetMapping; @@ -24,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + /** * 多联总单控制器 * @@ -38,6 +41,7 @@ public class MasterOrderController extends BladeController { private final IMasterOrderService masterOrderService; @GetMapping("/detail") @ApiOperationSupport(order = 1) @Operation(summary = "详情") public R detail(@RequestParam Long id) { return R.data(masterOrderService.detail(id)); } + @GetMapping("/carriers") @ApiOperationSupport(order = 2) @Operation(summary = "调度可选承运商") public R> carriers(@RequestParam Long id) { return R.data(masterOrderService.carriers(id)); } @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页") public R> list(MasterOrderVO query, Query page) { return R.data(masterOrderService.selectPage(Condition.getPage(page), query)); } @PostMapping("/submit") @ApiOperationSupport(order = 3) @Operation(summary = "确认创建或编辑") public R submit(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, false)); } @PostMapping("/draft") @ApiOperationSupport(order = 4) @Operation(summary = "暂存草稿") public R draft(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, true)); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index 50452e8..dbb4539 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -9,6 +9,7 @@ import org.springblade.core.mp.base.BaseService; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import java.util.List; @@ -24,6 +25,8 @@ public interface ILoadingManageService extends BaseService { LoadingManageVO detail(Long id); + List carrierContracts(List waybillIds); + boolean saveDraft(LoadingManage loadingManage); boolean submit(LoadingManage loadingManage); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java index bb78280..f178ad0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest; import org.springblade.transport.pojo.entity.MasterOrder; +import org.springblade.transport.pojo.vo.MasterOrderCarrierVO; import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.excel.MasterOrderWaybillExcel; @@ -18,6 +19,7 @@ import java.util.List; public interface IMasterOrderService extends BaseService { IPage selectPage(IPage page, MasterOrderVO query); MasterOrderVO detail(Long id); + List carriers(Long id); MasterOrderVO submit(MasterOrderVO masterOrder, boolean draft); MasterOrderVO copy(Long id); boolean removeMasterOrder(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index 0cae341..cf922ac 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -77,6 +77,9 @@ public interface IReceivablePayableDetailService extends BaseService waybillIds); - /** 关闭总单调度后按合同系统计费模式自动生成总单应收、应付明细。 */ + /** 完成配载单后按其记录的承运商合同自动生成应付明细。 */ + void generateForCompletedLoading(List waybillIds, Long carrierContractId); + + /** 关闭总单调度后生成总单客户合同应收,并按所属运单记录的承运商合同生成应付。 */ void generateForClosedMasterOrder(MasterOrder masterOrder); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index b1e79eb..d8c6f37 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -507,12 +507,15 @@ public class ContractManageServiceImpl extends BaseServiceImpllambdaQuery() .eq(ContractManage::getIsDeleted, 0) + .eq(Func.isNotEmpty(contractManage.getProjectId()), ContractManage::getProjectId, + contractManage.getProjectId()) + .isNull(Func.isEmpty(contractManage.getProjectId()), ContractManage::getProjectId) .eq(ContractManage::getContractCategory, contractManage.getContractCategory()) .eq(ContractManage::getPartyA, contractManage.getPartyA()) .eq(ContractManage::getPartyB, contractManage.getPartyB()) .ne(Func.isNotEmpty(contractManage.getId()), ContractManage::getId, contractManage.getId())); if (count > 0) { - throw new ServiceException("相同合同类别、甲方和乙方的合同已存在,不能重复提交"); + throw new ServiceException("相同项目、合同类别、甲方和乙方的合同已存在,不能重复提交"); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index b2a61a7..28cf212 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -17,10 +17,13 @@ import org.springblade.system.pojo.entity.Dept; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.mapper.LoadingManageMapper; import org.springblade.transport.mapper.WaybillMapper; +import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; +import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.support.TransportBusinessSupport; @@ -36,7 +39,10 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 配载管理 服务实现类 @@ -52,12 +58,14 @@ public class LoadingManageServiceImpl extends BaseServiceImpl carrierContracts(List waybillIds) { + return findCarrierContracts(waybillIds).stream().map(contract -> { + LoadingCarrierContractVO option = new LoadingCarrierContractVO(); + option.setId(contract.getId()); + option.setContractName(contract.getContractName()); + option.setCarrierName(contract.getPartyB()); + return option; + }).toList(); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean saveDraft(LoadingManage loadingManage) { @@ -96,6 +115,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl associatedWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); syncAssociatedWaybills(loadingManage, STATUS_COMPLETED, new ArrayList<>()); if (result) { - receivablePayableDetailService.generateForCompletedWaybills(associatedWaybillIds); + receivablePayableDetailService.generateForCompletedLoading( + associatedWaybillIds, loadingManage.getCarrierContractId()); } return result; } @@ -298,6 +323,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); if (Func.isEmpty(waybillIdList)) { return false; @@ -311,7 +337,12 @@ public class LoadingManageServiceImpl extends BaseServiceImpl Objects.equals(contract.getId(), loadingManage.getCarrierContractId())) + .findFirst() + .orElseThrow(() -> new ServiceException("所选承运商合同与运单绑定的客户合同不匹配或已失效")); + loadingManage.setCarrierName(carrierContract.getPartyB()); + } + + private void validateCompletedCarrierContract(LoadingManage loadingManage) { + if (Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) { + loadingManage.setCarrierContractId(null); + loadingManage.setCarrierName(null); + return; + } + if (Func.isEmpty(loadingManage.getCarrierContractId())) { + throw new ServiceException("配载单未记录承运商合同,请先重新派单并选择承运商合同"); + } + } + + private List findCarrierContracts(List waybillIds) { + if (Func.isEmpty(waybillIds)) { + return new ArrayList<>(); + } + List distinctWaybillIds = waybillIds.stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + if (Func.isEmpty(distinctWaybillIds)) { + return new ArrayList<>(); + } + List waybillList = waybillMapper.selectList(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .in(Waybill::getId, distinctWaybillIds)); + if (waybillList.size() != distinctWaybillIds.size()) { + throw new ServiceException("待配载运单不存在或已删除"); + } + waybillList.forEach(waybill -> + TransportBusinessSupport.assertCurrentDept(waybill.getDeptId(), "运单管理")); + if (waybillList.stream().anyMatch(waybill -> Func.isEmpty(waybill.getContractId()))) { + throw new ServiceException("所选运单存在未绑定客户合同的数据"); + } + List customerContractIds = waybillList.stream() + .map(Waybill::getContractId) + .distinct() + .toList(); + Map customerContractMap = contractManageService.listByIds(customerContractIds) + .stream() + .filter(contract -> Objects.equals(contract.getIsDeleted(), 0)) + .collect(Collectors.toMap(ContractManage::getId, Function.identity())); + if (customerContractMap.size() != customerContractIds.size()) { + throw new ServiceException("所选运单绑定的客户合同不存在或已删除"); + } + Waybill firstWaybill = waybillList.get(0); + ContractManage firstCustomerContract = customerContractMap.get(firstWaybill.getContractId()); + if (!Objects.equals(firstCustomerContract.getContractCategory(), "客户合同") + || Func.isEmpty(firstCustomerContract.getPartyA())) { + throw new ServiceException("所选运单绑定的客户合同信息不完整"); + } + boolean relationMismatch = waybillList.stream().anyMatch(waybill -> { + ContractManage customerContract = customerContractMap.get(waybill.getContractId()); + return !Objects.equals(customerContract.getContractCategory(), "客户合同") + || !Objects.equals(waybill.getProjectId(), firstWaybill.getProjectId()) + || !Objects.equals(customerContract.getPartyA(), firstCustomerContract.getPartyA()); + }); + if (relationMismatch) { + throw new ServiceException("所选运单的客户合同不属于同一项目和甲方,无法共用承运商合同"); + } + return contractManageService.list(Wrappers.lambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, firstWaybill.getProjectId()) + .eq(ContractManage::getPartyA, firstCustomerContract.getPartyA()) + .eq(ContractManage::getContractCategory, "承运商合同") + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) + .or().ne(ContractManage::getContractStage, "terminated")) + .orderByDesc(ContractManage::getCreateTime)); + } + private void syncAssociatedWaybills(LoadingManage loadingManage, String status, List oldWaybillIds) { List newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); if (Func.isNotEmpty(oldWaybillIds)) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index 8ec2c59..38cdfa5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -20,6 +20,7 @@ import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.TransportPlan; import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.MasterOrderCarrierVO; import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.service.IMasterOrderService; import org.springblade.transport.service.IContractManageService; @@ -77,6 +78,17 @@ public class MasterOrderServiceImpl extends BaseServiceImpl carriers(Long id) { + return availableCarrierContracts(getRequired(id)).stream().map(contract -> { + MasterOrderCarrierVO carrier = new MasterOrderCarrierVO(); + carrier.setContractId(contract.getId()); + carrier.setContractName(contract.getContractName()); + carrier.setCarrierName(contract.getPartyB()); + return carrier; + }).toList(); + } + @Override @Transactional(rollbackFor = Exception.class) public MasterOrderVO submit(MasterOrderVO request, boolean draft) { @@ -203,7 +215,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl> parseArray(String json) { if (Func.isEmpty(json)) return new ArrayList<>(); try { return JsonUtil.parse(json, List.class); } catch (Exception exception) { return new ArrayList<>(); } } private void validateDispatches(MasterOrder masterOrder, List> dispatches) { Map available = availableGoods(masterOrder); + Map availableCarrierContracts = availableCarrierContracts(masterOrder).stream() + .collect(java.util.stream.Collectors.toMap(ContractManage::getId, contract -> contract)); Map> usedBySegment = new LinkedHashMap<>(); for (Map dispatch : dispatches) { String segmentNo = string(dispatch, "segmentNo"); @@ -357,7 +371,9 @@ public class MasterOrderServiceImpl extends BaseServiceImpl used = usedBySegment.computeIfAbsent(segmentNo, value -> dispatchedGoods(masterOrder.getMasterNo(), value)); used.merge(key, quantity, BigDecimal::add); if (used.get(key).compareTo(available.get(key)) > 0) throw new ServiceException("货物【" + string(dispatch, "cargoName") + "】的调度数量超过可调度数量"); @@ -372,8 +388,45 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatched = dispatchedGoods(masterNo, segmentNo); return available.entrySet().stream().allMatch(item -> dispatched.getOrDefault(item.getKey(), BigDecimal.ZERO).compareTo(item.getValue()) >= 0); } - private void validateCarrier(Map dispatch) { + private List availableCarrierContracts(MasterOrder masterOrder) { + if (Func.isEmpty(masterOrder.getContractId())) { + throw new ServiceException("总单未绑定客户合同"); + } + ContractManage customerContract = contractManageService.getById(masterOrder.getContractId()); + if (customerContract == null || !Objects.equals(customerContract.getContractCategory(), "客户合同") + || Func.isEmpty(customerContract.getProjectId())) { + throw new ServiceException("总单绑定的客户合同不存在或项目信息不完整"); + } + if (!Objects.equals(masterOrder.getProjectId(), customerContract.getProjectId())) { + throw new ServiceException("总单项目与客户合同所属项目不一致"); + } + Map contractsByCarrier = new LinkedHashMap<>(); + contractManageService.list(new LambdaQueryWrapper() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, customerContract.getProjectId()) + .eq(ContractManage::getContractCategory, "承运商合同") + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) + .or().ne(ContractManage::getContractStage, "terminated")) + .orderByDesc(ContractManage::getCreateTime)) + .stream().filter(contract -> Func.isNotEmpty(contract.getPartyB())) + .forEach(contract -> contractsByCarrier.putIfAbsent(contract.getPartyB(), contract)); + return new ArrayList<>(contractsByCarrier.values()); + } + + private void validateCarrier(Map dispatch, Map availableCarrierContracts) { String carrierType = string(dispatch, "carrierType", "承运商"); + if ("自运".equals(carrierType)) { + dispatch.remove("carrierContractId"); + dispatch.remove("carrierName"); + } + String carrierName = string(dispatch, "carrierName"); + Long carrierContractId = longValue(dispatch, "carrierContractId"); + ContractManage carrierContract = carrierContractId == null ? null : availableCarrierContracts.get(carrierContractId); + if (!"自运".equals(carrierType) && (carrierContract == null + || !Objects.equals(carrierContract.getPartyB(), carrierName))) { + throw new ServiceException("所选承运商不属于总单客户合同对应项目的有效承运商合同乙方"); + } boolean road = string(dispatch, "transportType", "").toLowerCase().contains("road") || string(dispatch, "transportType", "").contains("公路"); if (!road) { if (Func.isEmpty(string(dispatch, "vehicleNo")) || Func.isEmpty(string(dispatch, "captainName")) || Func.isEmpty(string(dispatch, "driverPhone")) || Func.isEmpty(string(dispatch, "containerNo")) || Func.isEmpty(string(dispatch, "cabinNo")) || Func.isEmpty(string(dispatch, "mileage")) || decimal(dispatch, "mileage").compareTo(BigDecimal.ZERO) <= 0 || ("承运商".equals(carrierType) && Func.isEmpty(string(dispatch, "carrierName")))) { @@ -453,10 +506,11 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "driverPhone", ""), string(dispatch, "vehicleNo", ""), string(dispatch, "captainName", ""), string(dispatch, "containerNo", ""), string(dispatch, "cabinNo", "")); } + private String waybillGroupKey(Map dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierContractId", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "driverPhone", ""), string(dispatch, "vehicleNo", ""), string(dispatch, "captainName", ""), string(dispatch, "containerNo", ""), string(dispatch, "cabinNo", "")); } private String joinGoodsField(List> dispatches, String field) { return dispatches.stream().map(item -> string(item, field, "")).filter(Func::isNotEmpty).distinct().reduce((left, right) -> left + "、" + right).orElse(""); } private BigDecimal decimal(Map values, String key) { try { return new BigDecimal(string(values, key, "0")); } catch (Exception exception) { return BigDecimal.ZERO; } } private BigDecimal nullableDecimal(Map values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return new BigDecimal(value); } catch (Exception exception) { return null; } } + private Long longValue(Map values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return Long.valueOf(value); } catch (Exception exception) { return null; } } private String goodsKey(Map values) { return goodsKey(string(values, "cargoName"), string(values, "cargoType")); } private String goodsKey(String cargoName, String cargoType) { return String.valueOf(cargoName) + "\u0000" + String.valueOf(cargoType); } private LocalDate date(Map values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return LocalDate.parse(value.substring(0, 10)); } catch (Exception exception) { throw new ServiceException("日期格式不正确"); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 8a0135d..4aef6e3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -84,6 +84,10 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 应收应付明细服务实现类 @@ -535,25 +539,46 @@ public class ReceivablePayableDetailServiceImpl @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void generateForCompletedWaybills(List waybillIds) { - if (Func.isEmpty(waybillIds)) return; - Long currentUserId = AuthUtil.getUserId(); - Date generateTime = new Date(); - for (Long waybillId : waybillIds) { - Waybill waybill = waybillService.getById(waybillId); - if (waybill == null) continue; - ContractManage receivableContract = Func.isEmpty(waybill.getContractId()) - ? null : contractManageService.getById(waybill.getContractId()); + generateAutomaticWaybillDetails(loadWaybills(waybillIds), true, + this::resolveWaybillCarrierContract, AuthUtil.getUserId(), new Date()); + } + + @Override + @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, + rollbackFor = Exception.class) + public void generateForCompletedLoading(List waybillIds, Long carrierContractId) { + ContractManage carrierContract = carrierContractId == null + ? null : contractManageService.getById(carrierContractId); + if (carrierContractId != null && (carrierContract == null + || !"承运商合同".equals(carrierContract.getContractCategory()))) { + throw new ServiceException("配载单记录的承运商合同不存在或合同类别不正确"); + } + generateAutomaticWaybillDetails(loadWaybills(waybillIds), true, + waybill -> carrierContract, AuthUtil.getUserId(), new Date()); + } + + private List loadWaybills(List waybillIds) { + if (Func.isEmpty(waybillIds)) return List.of(); + return waybillService.list(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .in(Waybill::getId, waybillIds)); + } + + private void generateAutomaticWaybillDetails(List waybills, boolean generateReceivable, + Function payableContractResolver, + Long currentUserId, Date generateTime) { + for (Waybill waybill : waybills) { try { - if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory()) - && isSystemGeneration(receivableContract)) { - generateCompletedWaybillDetail(waybill, receivableContract, "receivable", currentUserId, generateTime); - } - if (Func.isNotEmpty(waybill.getCarrierName())) { - ContractManage payableContract = findCarrierContract(waybill); - if (payableContract != null && isSystemGeneration(payableContract)) { - generateCompletedWaybillDetail(waybill, payableContract, "payable", currentUserId, generateTime); - } + if (generateReceivable) { + ContractManage receivableContract = Func.isEmpty(waybill.getContractId()) + ? null : contractManageService.getById(waybill.getContractId()); + generateAutomaticWaybillDetail(waybill, receivableContract, "receivable", + currentUserId, generateTime); } + ContractManage payableContract = payableContractResolver == null + ? null : payableContractResolver.apply(waybill); + generateAutomaticWaybillDetail(waybill, payableContract, "payable", + currentUserId, generateTime); } catch (Exception exception) { log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, receivableContractId:{}, failureReason:{}", waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception); @@ -565,24 +590,24 @@ public class ReceivablePayableDetailServiceImpl } } - private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType, - Long currentUserId, Date generateTime) { - if (existsByWaybill(waybill.getId(), settlementType)) return; - String planId = matchedPlanId(waybill, contract); - if (Func.isEmpty(planId)) return; - // 自动生成只按合同默认计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 - List matchedFees = calculatedFees(waybill, contract, planId, true); - if (matchedFees.isEmpty()) return; - BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); - ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees, contractUnitPrice); - save(detail); - for (ReceivablePayableCargoFee fee : matchedFees) { - ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); - copy.setId(null); - copy.setDetailId(detail.getId()); - fillGeneratedAuditFields(copy, currentUserId, generateTime); - cargoFeeMapper.insert(copy); + private void generateAutomaticWaybillDetail(Waybill waybill, ContractManage contract, String settlementType, + Long currentUserId, Date generateTime) { + generateAutomaticDetail(contract, settlementType, + () -> existsByWaybill(waybill.getId(), settlementType), List.of(waybill), + (fees, unitPrice) -> buildDetail(waybill, contract, settlementType, fees, unitPrice), + false, currentUserId, generateTime); + } + + private ContractManage resolveWaybillCarrierContract(Waybill waybill) { + if (Func.isNotEmpty(waybill.getLoadingNo())) return null; + if (Func.isNotEmpty(waybill.getCarrierContractId())) { + ContractManage contract = contractManageService.getById(waybill.getCarrierContractId()); + if (contract == null || !"承运商合同".equals(contract.getContractCategory())) { + throw new ServiceException("运单记录的承运商合同不存在或合同类别不正确"); + } + return contract; } + return findCarrierContract(waybill); } private ContractManage findCarrierContract(Waybill waybill) { @@ -611,39 +636,14 @@ public class ReceivablePayableDetailServiceImpl @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void generateForClosedMasterOrder(MasterOrder masterOrder) { - if (masterOrder == null || Func.isEmpty(masterOrder.getId()) || Func.isEmpty(masterOrder.getContractId())) { + if (masterOrder == null || Func.isEmpty(masterOrder.getId()) || Func.isEmpty(masterOrder.getMasterNo())) { return; } - ContractManage contract = contractManageService.getById(masterOrder.getContractId()); - if (contract == null || !isSystemGeneration(contract)) return; Long currentUserId = AuthUtil.getUserId(); Date generateTime = new Date(); try { - List masterGoods = masterOrderGoods(masterOrder); - if (masterGoods.isEmpty()) return; - List matchedFees = new ArrayList<>(); - for (Waybill masterGoodsItem : masterGoods) { - String planId = matchedPlanId(masterGoodsItem, contract); - if (Func.isEmpty(planId)) continue; - matchedFees.addAll(calculatedFees(masterGoodsItem, contract, planId, true)); - } - if (matchedFees.isEmpty()) return; - normalizeMasterFeeLines(matchedFees); - BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); - for (String settlementType : List.of("payable", "receivable")) { - if (existsByMasterOrder(masterOrder.getMasterNo(), settlementType)) continue; - ReceivablePayableDetail detail = buildMasterOrderDetail(masterOrder, contract, masterGoods, - settlementType, matchedFees, contractUnitPrice); - save(detail); - for (ReceivablePayableCargoFee fee : matchedFees) { - ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); - copy.setId(null); - copy.setDetailId(detail.getId()); - copy.setWaybillId(null); - fillGeneratedAuditFields(copy, currentUserId, generateTime); - cargoFeeMapper.insert(copy); - } - } + generateClosedMasterOrderReceivable(masterOrder, currentUserId, generateTime); + generateClosedMasterOrderPayables(masterOrder, currentUserId, generateTime); } catch (Exception exception) { log.error("自动生成总单费用明细失败,masterOrderId:{}, masterNo:{}, contractId:{}, failureReason:{}", masterOrder.getId(), masterOrder.getMasterNo(), masterOrder.getContractId(), exception.getMessage(), exception); @@ -654,6 +654,85 @@ public class ReceivablePayableDetailServiceImpl } } + private void generateClosedMasterOrderReceivable(MasterOrder masterOrder, Long currentUserId, + Date generateTime) { + if (Func.isEmpty(masterOrder.getContractId())) return; + ContractManage customerContract = contractManageService.getById(masterOrder.getContractId()); + List masterGoods = masterOrderGoods(masterOrder); + if (masterGoods.isEmpty()) return; + generateAutomaticDetail(customerContract, "receivable", + () -> existsByMasterOrder(masterOrder.getMasterNo(), "receivable"), masterGoods, + (fees, unitPrice) -> { + normalizeMasterFeeLines(fees); + return buildMasterOrderDetail(masterOrder, customerContract, masterGoods, + "receivable", fees, unitPrice); + }, true, currentUserId, generateTime); + } + + private void generateClosedMasterOrderPayables(MasterOrder masterOrder, Long currentUserId, + Date generateTime) { + List waybills = waybillService.list(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .eq(Waybill::getMasterNo, masterOrder.getMasterNo()) + .isNotNull(Waybill::getCarrierContractId)); + if (waybills.isEmpty()) return; + Map carrierContracts = contractManageService.listByIds(waybills.stream() + .map(Waybill::getCarrierContractId).distinct().toList()).stream() + .collect(Collectors.toMap(ContractManage::getId, contract -> contract)); + generateAutomaticWaybillDetails(waybills, false, waybill -> { + ContractManage carrierContract = carrierContracts.get(waybill.getCarrierContractId()); + if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) { + throw new ServiceException("运单【" + waybill.getWaybillNo() + "】记录的承运商合同不存在或合同类别不正确"); + } + return carrierContract; + }, currentUserId, generateTime); + } + + private boolean isAutomaticContract(ContractManage contract, String settlementType) { + if (contract == null) return false; + String expectedCategory = "payable".equals(settlementType) ? "承运商合同" : "客户合同"; + return Objects.equals(expectedCategory, contract.getContractCategory()) && isSystemGeneration(contract); + } + + private List calculateAutomaticFees(List waybills, + ContractManage contract) { + List matchedFees = new ArrayList<>(); + for (Waybill waybill : waybills) { + String planId = matchedPlanId(waybill, contract); + if (Func.isEmpty(planId)) continue; + // 自动生成统一使用合同默认计费方案,且禁止回退读取运单自身其他费用。 + matchedFees.addAll(calculatedFees(waybill, contract, planId, true)); + } + return matchedFees; + } + + private void generateAutomaticDetail(ContractManage contract, String settlementType, + BooleanSupplier exists, List billingWaybills, + BiFunction, BigDecimal, + ReceivablePayableDetail> detailBuilder, + boolean clearWaybillId, Long currentUserId, Date generateTime) { + if (!isAutomaticContract(contract, settlementType) || exists.getAsBoolean()) return; + List matchedFees = calculateAutomaticFees(billingWaybills, contract); + if (matchedFees.isEmpty()) return; + BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); + ReceivablePayableDetail detail = detailBuilder.apply(matchedFees, contractUnitPrice); + saveAutomaticDetail(detail, matchedFees, clearWaybillId, currentUserId, generateTime); + } + + private void saveAutomaticDetail(ReceivablePayableDetail detail, + List fees, boolean clearWaybillId, + Long currentUserId, Date generateTime) { + save(detail); + for (ReceivablePayableCargoFee fee : fees) { + ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); + copy.setId(null); + copy.setDetailId(detail.getId()); + if (clearWaybillId) copy.setWaybillId(null); + fillGeneratedAuditFields(copy, currentUserId, generateTime); + cargoFeeMapper.insert(copy); + } + } + private boolean isSystemGeneration(ContractManage contract) { if (Func.isNotEmpty(contract.getFeeGenerationMode())) { return "system".equalsIgnoreCase(contract.getFeeGenerationMode()) || "系统生成".equals(contract.getFeeGenerationMode()); @@ -833,7 +912,8 @@ public class ReceivablePayableDetailServiceImpl private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String settlementType, List fees, BigDecimal unitPrice) { - BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) + .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP); ReceivablePayableDetail detail = new ReceivablePayableDetail(); @@ -1008,8 +1088,9 @@ public class ReceivablePayableDetailServiceImpl if (plan == null || !(plan.get("rules") instanceof List)) { return matchOnly ? List.of() : buildCargoFees(waybill); } - List result = new ArrayList<>(); - int line = 1; + Map, ReceivablePayableCargoFee> feesByCargo = new LinkedHashMap<>(); + Map, Map> feeItemsByCargo = new LinkedHashMap<>(); + Set> freightBillingCargoKeys = new LinkedHashSet<>(); for (Object value : (List) plan.get("rules")) { if (!(value instanceof Map raw)) continue; Map rule = new LinkedHashMap<>(); @@ -1020,25 +1101,69 @@ public class ReceivablePayableDetailServiceImpl if (amount == null) continue; String feeItem = stringValue(rule, "feeItem", "费用"); Map feeGoods = summarizeFeeGoods(rule, feeWaybill); - ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); - fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); - fee.setCargoName(feeGoods.getOrDefault("cargoName", feeWaybill.getCargoName())); - fee.setCargoType(feeGoods.getOrDefault("cargoType", feeWaybill.getCargoType())); - fee.setSpecification(feeGoods.getOrDefault("specification", feeWaybill.getSpecification())); - fee.setModel(feeGoods.getOrDefault("model", feeWaybill.getModel())); - fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); - fee.setTransportQuantity(measure(rule, feeWaybill)); - fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", feeWaybill.getQuantityUnit())); - fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); - fee.setMileage(normalizeGeneratedMileage(feeWaybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); - fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount))); - fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", feeWaybill.getRemark())); - result.add(fee); + List cargoKey = cargoFeeKey(feeWaybill, feeGoods); + ReceivablePayableCargoFee fee = feesByCargo.computeIfAbsent(cargoKey, + key -> buildCalculatedCargoFee(waybill, feeWaybill, feeGoods, rule)); + Map feeItems = feeItemsByCargo.computeIfAbsent(cargoKey, + key -> new LinkedHashMap<>()); + feeItems.merge(feeItem, amount, BigDecimal::add); + fee.setFeeItemsJson(JsonUtil.toJson(feeItems)); + fee.setOriginalAmount(money(fee.getOriginalAmount()).add(amount)); + fee.setAfterAmount(fee.getOriginalAmount()); + if (isFreightRule(rule)) { + fee.setFreightAmount(money(fee.getFreightAmount()).add(amount)); + if (freightBillingCargoKeys.add(cargoKey)) { + fillCalculatedBillingFields(fee, feeWaybill, rule); + } + } } } + List result = new ArrayList<>(feesByCargo.values()); + for (int index = 0; index < result.size(); index++) { + ReceivablePayableCargoFee fee = result.get(index); + fee.setLineNo(String.format("%04d", index + 1)); + } return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result; } + private ReceivablePayableCargoFee buildCalculatedCargoFee(Waybill waybill, Waybill feeWaybill, + Map feeGoods, Map rule) { + ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); + fee.setWaybillId(waybill.getId()); + fee.setCargoName(feeGoods.getOrDefault("cargoName", feeWaybill.getCargoName())); + fee.setCargoType(feeGoods.getOrDefault("cargoType", feeWaybill.getCargoType())); + fee.setSpecification(feeGoods.getOrDefault("specification", feeWaybill.getSpecification())); + fee.setModel(feeGoods.getOrDefault("model", feeWaybill.getModel())); + fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", feeWaybill.getQuantityUnit())); + fillCalculatedBillingFields(fee, feeWaybill, rule); + fee.setMileage(normalizeGeneratedMileage(feeWaybill.getMileage())); + fee.setFreightAmount(BigDecimal.ZERO); + fee.setOriginalAmount(BigDecimal.ZERO); + fee.setAdjustAmount(BigDecimal.ZERO); + fee.setAfterAmount(BigDecimal.ZERO); + fee.setRemark(stringValue(rule, "remark", feeWaybill.getRemark())); + return fee; + } + + private void fillCalculatedBillingFields(ReceivablePayableCargoFee fee, Waybill feeWaybill, + Map rule) { + fee.setBillingFactor(stringValue(rule, "billingElement", "")); + fee.setBillingType(stringValue(rule, "billingType", "")); + fee.setTransportQuantity(measure(rule, feeWaybill)); + fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit())); + fee.setUnitPrice(decimal(rule.get("unitPrice"))); + } + + private List cargoFeeKey(Waybill waybill, Map feeGoods) { + return List.of( + Objects.toString(feeGoods.getOrDefault("cargoName", waybill.getCargoName()), ""), + Objects.toString(feeGoods.getOrDefault("cargoType", waybill.getCargoType()), ""), + Objects.toString(feeGoods.getOrDefault("specification", waybill.getSpecification()), ""), + Objects.toString(feeGoods.getOrDefault("model", waybill.getModel()), ""), + Objects.toString(feeGoods.getOrDefault("quantityUnit", waybill.getQuantityUnit()), ""), + money(waybill.getQuantity()).stripTrailingZeros().toPlainString()); + } + private List feeWaybills(Map rule, Waybill waybill) { String element = stringValue(rule, "billingElement", "按重量"); if (!List.of("按重量", "按体积", "按吨·公里", "按数量").contains(element)) { @@ -1354,22 +1479,26 @@ public class ReceivablePayableDetailServiceImpl if (rules.isEmpty()) { throw new ServiceException("未找到费用明细对应的合同计费规则,请先更新费用"); } - Map> results = new LinkedHashMap<>(); + Map calculatedAmounts = new LinkedHashMap<>(); + BigDecimal calculatedFreight = BigDecimal.ZERO; + boolean freightRuleMatched = false; for (Map rule : rules) { BigDecimal amount = calculateRule(rule, adjustedWaybill); - if (amount != null) results.putIfAbsent(money(amount), rule); + if (amount == null) continue; + String feeItem = stringValue(rule, "feeItem", "费用"); + calculatedAmounts.merge(feeItem, amount, BigDecimal::add); + if (isFreightRule(rule)) { + calculatedFreight = calculatedFreight.add(amount); + freightRuleMatched = true; + } } - if (results.size() != 1) { - throw new ServiceException("费用明细对应多个计费结果,无法唯一试算,请先更新费用"); + if (calculatedAmounts.isEmpty()) { + throw new ServiceException("费用明细对应的合同计费规则无法试算,请先更新费用"); } - Map.Entry> calculated = results.entrySet().iterator().next(); - BigDecimal amount = calculated.getKey(); - Map rule = calculated.getValue(); - String feeItem = stringValue(rule, "feeItem", "费用"); Map calculatedFeeItems = new LinkedHashMap<>(feeItems); - calculatedFeeItems.put(feeItem, amount); - BigDecimal calculatedFreight = isFreightRule(rule) ? amount : freightAmount; - return new AdjustedFeeCalculation(calculatedFreight, calculatedFeeItems); + calculatedAmounts.forEach(calculatedFeeItems::put); + return new AdjustedFeeCalculation(freightRuleMatched ? calculatedFreight : freightAmount, + calculatedFeeItems); } private Waybill adjustedWaybill(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee, @@ -1402,28 +1531,38 @@ public class ReceivablePayableDetailServiceImpl private List> matchingAdjustedRules(ContractManage contract, ReceivablePayableCargoFee fee, Waybill waybill) { Set feeItemNames = parseMap(fee.getFeeItemsJson()).keySet(); - List> candidates = new ArrayList<>(); + List> firstCandidates = List.of(); + List> defaultCandidates = List.of(); + List> billingMatchedCandidates = List.of(); for (Map plan : parseList(contract.getBillingPlanJson())) { if (!(plan.get("rules") instanceof List rules)) continue; + List> candidates = new ArrayList<>(); for (Object value : rules) { if (!(value instanceof Map raw)) continue; Map rule = new LinkedHashMap<>(); raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); - if (!Objects.equals(stringValue(rule, "billingElement"), fee.getBillingFactor()) - || !Objects.equals(stringValue(rule, "billingType"), fee.getBillingType()) - || !feeItemNames.contains(stringValue(rule, "feeItem"))) continue; - if (Func.isNotEmpty(fee.getPriceUnit()) - && !Objects.equals(stringValue(rule, "billingUnit"), fee.getPriceUnit())) continue; + if (!feeItemNames.contains(stringValue(rule, "feeItem")) || !matchesRule(rule, waybill)) continue; candidates.add(rule); } + if (candidates.isEmpty()) continue; + if (firstCandidates.isEmpty()) firstCandidates = candidates; + if (isDefaultPlan(plan)) defaultCandidates = candidates; + if (candidates.stream().anyMatch(rule -> matchesBillingFields(rule, fee))) { + if (billingMatchedCandidates.isEmpty() || isDefaultPlan(plan)) { + billingMatchedCandidates = candidates; + } + } } - List> unitPriceMatched = candidates.stream() - .filter(rule -> decimal(rule.get("unitPrice")).compareTo(money(fee.getUnitPrice())) == 0) - .toList(); - if (!unitPriceMatched.isEmpty()) candidates = unitPriceMatched; - List> conditionMatched = candidates.stream() - .filter(rule -> matchesRule(rule, waybill)).toList(); - return conditionMatched.isEmpty() ? candidates : conditionMatched; + if (!billingMatchedCandidates.isEmpty()) return billingMatchedCandidates; + return defaultCandidates.isEmpty() ? firstCandidates : defaultCandidates; + } + + private boolean matchesBillingFields(Map rule, ReceivablePayableCargoFee fee) { + if (!Objects.equals(stringValue(rule, "billingElement"), fee.getBillingFactor()) + || !Objects.equals(stringValue(rule, "billingType"), fee.getBillingType())) return false; + if (Func.isNotEmpty(fee.getPriceUnit()) + && !Objects.equals(stringValue(rule, "billingUnit"), fee.getPriceUnit())) return false; + return decimal(rule.get("unitPrice")).compareTo(money(fee.getUnitPrice())) == 0; } private BigDecimal adjustedAfterAmount(BigDecimal freightAmount, Map feeItems) { @@ -1488,7 +1627,8 @@ public class ReceivablePayableDetailServiceImpl List fees = calculatedFees(waybill, contract, billingPlanId); cargoFeeMapper.delete(Wrappers.lambdaQuery().eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); fees.forEach(fee -> { fee.setDetailId(detail.getId()); cargoFeeMapper.insert(fee); }); - BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) + .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); detail.setFreightAmount(freight); detail.setOtherFeeAmount(total.subtract(freight)); @@ -1517,8 +1657,8 @@ public class ReceivablePayableDetailServiceImpl fee.setWaybillId(null); cargoFeeMapper.insert(fee); }); - BigDecimal freight = fees.stream().filter(this::isFreight) - .map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) + .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); detail.setFreightAmount(freight); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index b9df29f..d3acfab 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -235,6 +235,7 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setCarrierType(source.getCarrierType()); target.setCarrierId(source.getCarrierId()); target.setCarrierName(source.getCarrierName()); + target.setCarrierContractId(source.getCarrierContractId()); target.setDriverId(source.getDriverId()); target.setDriverName(source.getDriverName()); target.setDriverPhone(source.getDriverPhone()); diff --git a/doc/sql/transport/blade_loading_manage_20260804.sql b/doc/sql/transport/blade_loading_manage_20260804.sql index 3096e6d..ffadac6 100644 --- a/doc/sql/transport/blade_loading_manage_20260804.sql +++ b/doc/sql/transport/blade_loading_manage_20260804.sql @@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` ( `escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号', `carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型', `carrier_name` varchar(100) DEFAULT NULL COMMENT '承运商', + `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID', `departure_address` varchar(100) DEFAULT NULL COMMENT '发货地', `transit_address` varchar(200) DEFAULT NULL COMMENT '途经地', `arrival_address` varchar(100) DEFAULT NULL COMMENT '到货地', @@ -47,6 +48,7 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` ( `business_status` varchar(100) DEFAULT NULL COMMENT '业务状态', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_loading_manage_no` (`loading_no`) USING BTREE, + KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE, KEY `idx_loading_manage_dept` (`dept_id`) USING BTREE, KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配载管理'; diff --git a/doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql b/doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql new file mode 100644 index 0000000..97efaf1 --- /dev/null +++ b/doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql @@ -0,0 +1,3 @@ +ALTER TABLE `blade_loading_manage` + ADD COLUMN `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID' AFTER `carrier_name`, + ADD KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE; diff --git a/doc/sql/transport/blade_tms_business.sql b/doc/sql/transport/blade_tms_business.sql index 3d5ec50..f6f44c8 100644 --- a/doc/sql/transport/blade_tms_business.sql +++ b/doc/sql/transport/blade_tms_business.sql @@ -244,6 +244,7 @@ CREATE TABLE `blade_waybill` ( `carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型', `carrier_id` bigint(20) DEFAULT NULL COMMENT '承运商ID', `carrier_name` varchar(100) DEFAULT NULL COMMENT '承运商名称', + `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID', `driver_id` bigint(20) DEFAULT NULL COMMENT '司机ID', `driver_name` varchar(100) DEFAULT NULL COMMENT '司机姓名', `driver_phone` varchar(50) DEFAULT NULL COMMENT '司机手机号', @@ -285,6 +286,7 @@ CREATE TABLE `blade_waybill` ( PRIMARY KEY (`id`) USING BTREE, KEY `idx_waybill_dept` (`dept_id`) USING BTREE, KEY `idx_waybill_plan_id` (`plan_id`) USING BTREE, + KEY `idx_waybill_carrier_contract_id` (`carrier_contract_id`) USING BTREE, KEY `idx_waybill_create_time` (`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运单管理'; @@ -319,6 +321,7 @@ CREATE TABLE `blade_loading_manage` ( `escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号', `carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型', `carrier_name` varchar(100) DEFAULT NULL COMMENT '承运商', + `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID', `departure_address` varchar(100) DEFAULT NULL COMMENT '发货地', `transit_address` varchar(200) DEFAULT NULL COMMENT '途经地', `arrival_address` varchar(100) DEFAULT NULL COMMENT '到货地', @@ -341,6 +344,7 @@ CREATE TABLE `blade_loading_manage` ( `business_status` varchar(100) DEFAULT NULL COMMENT '业务状态', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_loading_manage_no` (`loading_no`) USING BTREE, + KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE, KEY `idx_loading_manage_dept` (`dept_id`) USING BTREE, KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配载管理'; diff --git a/doc/sql/transport/blade_waybill_carrier_contract_20260821.sql b/doc/sql/transport/blade_waybill_carrier_contract_20260821.sql new file mode 100644 index 0000000..02c5261 --- /dev/null +++ b/doc/sql/transport/blade_waybill_carrier_contract_20260821.sql @@ -0,0 +1,3 @@ +ALTER TABLE `blade_waybill` + ADD COLUMN `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID' AFTER `carrier_name`, + ADD KEY `idx_waybill_carrier_contract_id` (`carrier_contract_id`) USING BTREE; From 3e265fc71ee33df2dc9984d90a6e80731be69fbf Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 22 Aug 2026 17:25:26 +0800 Subject: [PATCH 035/114] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=94=B6=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/dto/BillLedgerSaveRequest.java | 34 + .../pojo/dto/BillPaymentSaveRequest.java | 45 ++ .../pojo/dto/BillPaymentStatusRequest.java | 39 + .../dto/InvoiceApplicationSaveRequest.java | 82 +++ .../dto/InvoiceApplicationStatusRequest.java | 43 ++ .../pojo/dto/InvoiceReceiptSaveRequest.java | 61 ++ .../pojo/dto/InvoiceReceiptStatusRequest.java | 43 ++ .../dto/PaymentApplicationInvoiceRequest.java | 41 ++ .../dto/PaymentApplicationSaveRequest.java | 61 ++ .../dto/PaymentApplicationStatusRequest.java | 33 + .../dto/ReceiptClaimAttachmentsRequest.java | 48 ++ .../pojo/dto/ReceiptClaimRequest.java | 63 ++ .../pojo/dto/ReceiptFlowSyncRequest.java | 69 ++ .../transport/pojo/entity/BillLedger.java | 42 ++ .../pojo/entity/BillLedgerUsage.java | 29 + .../transport/pojo/entity/BillPayment.java | 57 ++ .../pojo/entity/InvoiceApplication.java | 83 +++ .../pojo/entity/InvoiceApplicationDetail.java | 68 ++ .../pojo/entity/InvoiceApplicationLine.java | 58 ++ .../pojo/entity/InvoiceApplicationRecord.java | 53 ++ .../entity/InvoiceApplicationSettlement.java | 52 ++ .../pojo/entity/InvoiceApplicationSheet.java | 49 ++ .../transport/pojo/entity/InvoiceReceipt.java | 83 +++ .../pojo/entity/InvoiceReceiptRecord.java | 58 ++ .../pojo/entity/InvoiceReceiptSettlement.java | 58 ++ .../pojo/entity/KingdeeInvoicePool.java | 68 ++ .../pojo/entity/KingdeeReceiptFlow.java | 64 ++ .../pojo/entity/PaymentApplication.java | 76 ++ .../entity/PaymentApplicationInvoice.java | 47 ++ .../pojo/entity/PaymentApplicationRecord.java | 43 ++ .../transport/pojo/entity/ReceiptClaim.java | 74 ++ .../pojo/entity/ReceiptClaimSettlement.java | 63 ++ .../pojo/entity/ReceiptFlowRecord.java | 64 ++ .../transport/pojo/vo/BillLedgerVO.java | 31 + .../transport/pojo/vo/BillPaymentVO.java | 46 ++ .../pojo/vo/InvoiceApplicationSheetVO.java | 47 ++ .../pojo/vo/InvoiceApplicationVO.java | 56 ++ .../transport/pojo/vo/InvoiceReceiptVO.java | 56 ++ .../pojo/vo/PaymentApplicationVO.java | 47 ++ .../pojo/vo/ReceiptClaimRecordVO.java | 93 +++ .../transport/pojo/vo/ReceiptFlowVO.java | 73 ++ .../controller/BillLedgerController.java | 82 +++ .../controller/BillPaymentController.java | 118 +++ .../InvoiceApplicationController.java | 154 ++++ .../controller/InvoiceReceiptController.java | 157 ++++ .../PaymentApplicationController.java | 87 +++ .../ReceiptClaimRecordController.java | 90 +++ .../controller/ReceiptFlowController.java | 102 +++ .../transport/mapper/BillLedgerMapper.java | 14 + .../mapper/BillLedgerUsageMapper.java | 14 + .../transport/mapper/BillPaymentMapper.java | 35 + .../InvoiceApplicationDetailMapper.java | 39 + .../mapper/InvoiceApplicationLineMapper.java | 39 + .../mapper/InvoiceApplicationMapper.java | 39 + .../InvoiceApplicationRecordMapper.java | 39 + .../InvoiceApplicationSettlementMapper.java | 39 + .../mapper/InvoiceApplicationSheetMapper.java | 39 + .../mapper/InvoiceReceiptMapper.java | 39 + .../mapper/InvoiceReceiptRecordMapper.java | 39 + .../InvoiceReceiptSettlementMapper.java | 39 + .../mapper/KingdeeInvoicePoolMapper.java | 39 + .../mapper/KingdeeReceiptFlowMapper.java | 39 + .../PaymentApplicationInvoiceMapper.java | 29 + .../mapper/PaymentApplicationMapper.java | 29 + .../PaymentApplicationRecordMapper.java | 29 + .../transport/mapper/ReceiptClaimMapper.java | 50 ++ .../transport/mapper/ReceiptClaimMapper.xml | 114 +++ .../mapper/ReceiptClaimSettlementMapper.java | 39 + .../mapper/ReceiptFlowRecordMapper.java | 39 + .../transport/service/IBillLedgerService.java | 24 + .../service/IBillPaymentService.java | 45 ++ .../service/IInvoiceApplicationService.java | 57 ++ .../service/IInvoiceReceiptService.java | 69 ++ .../service/IPaymentApplicationService.java | 40 + .../service/IReceiptClaimRecordService.java | 49 ++ .../service/IReceiptFlowService.java | 54 ++ .../service/impl/BillLedgerServiceImpl.java | 302 ++++++++ .../service/impl/BillPaymentServiceImpl.java | 343 +++++++++ .../impl/InvoiceApplicationServiceImpl.java | 643 ++++++++++++++++ .../impl/InvoiceReceiptServiceImpl.java | 684 ++++++++++++++++++ .../impl/PaymentApplicationServiceImpl.java | 392 ++++++++++ .../impl/ReceiptClaimRecordServiceImpl.java | 259 +++++++ .../service/impl/ReceiptFlowServiceImpl.java | 504 +++++++++++++ .../transport/wrapper/BillLedgerWrapper.java | 44 ++ .../transport/wrapper/BillPaymentWrapper.java | 57 ++ .../wrapper/InvoiceApplicationWrapper.java | 66 ++ .../wrapper/InvoiceReceiptWrapper.java | 67 ++ .../wrapper/PaymentApplicationWrapper.java | 59 ++ .../transport/wrapper/ReceiptFlowWrapper.java | 67 ++ .../transport/blade_bill_ledger_20260821.sql | 113 +++ .../transport/blade_bill_payment_20260822.sql | 79 ++ .../blade_invoice_application_20260821.sql | 187 +++++ .../blade_invoice_receipt_20260821.sql | 145 ++++ .../blade_payment_application_20260821.sql | 46 ++ .../transport/blade_receipt_flow_20260821.sql | 114 +++ ...ade_receipt_flow_claim_record_20260821.sql | 107 +++ 96 files changed, 8527 insertions(+) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillLedgerSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationInvoiceRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimAttachmentsRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptFlowSyncRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedger.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedgerUsage.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillPayment.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationInvoice.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimSettlement.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptFlowRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillLedgerVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillPaymentVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillPaymentController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerUsageMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillPaymentMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationDetailMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationLineMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSettlementMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSheetMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptSettlementMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationInvoiceMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimSettlementMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptFlowRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillPaymentService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillLedgerWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillPaymentWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceApplicationWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceReceiptWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java create mode 100644 doc/sql/transport/blade_bill_ledger_20260821.sql create mode 100644 doc/sql/transport/blade_bill_payment_20260822.sql create mode 100644 doc/sql/transport/blade_invoice_application_20260821.sql create mode 100644 doc/sql/transport/blade_invoice_receipt_20260821.sql create mode 100644 doc/sql/transport/blade_payment_application_20260821.sql create mode 100644 doc/sql/transport/blade_receipt_flow_20260821.sql create mode 100644 doc/sql/transport/blade_receipt_flow_claim_record_20260821.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillLedgerSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillLedgerSaveRequest.java new file mode 100644 index 0000000..bc68385 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillLedgerSaveRequest.java @@ -0,0 +1,34 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 汇票台账保存请求。 @author Chill */ +@Data +public class BillLedgerSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String billNo; + private Long issuerId; + private String receiverName; + private String billType; + private BigDecimal faceAmount; + private LocalDate issueDate; + private LocalDate maturityDate; + private String availableDeptIdsJson; + private String availableDeptNames; + private Long feeBearerId; + private BigDecimal confirmedDiscountRate; + private String issuingBank; + private BigDecimal bankDiscountReferenceRate; + private String attachmentsJson; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentSaveRequest.java new file mode 100644 index 0000000..08b5724 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentSaveRequest.java @@ -0,0 +1,45 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 汇票付款保存请求。 @author Chill */ +@Data +public class BillPaymentSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long billLedgerId; + private BigDecimal usedAmount; + private LocalDate paymentDate; + private String attachmentsJson; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentStatusRequest.java new file mode 100644 index 0000000..4ce0b87 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/BillPaymentStatusRequest.java @@ -0,0 +1,39 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** 汇票付款状态请求。 @author Chill */ +@Data +public class BillPaymentStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationSaveRequest.java new file mode 100644 index 0000000..b6b5274 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationSaveRequest.java @@ -0,0 +1,82 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 开票申请保存请求 + * + * @author Chill + */ +@Data +public class InvoiceApplicationSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String invoiceType; + private String departmentEmails; + private Long receiverInvoiceInfoId; + private String contactName; + private String contactPhone; + private String email; + private String attachmentsJson; + private String remark; + private List settlements; + private List detailIds; + private List sheets; + + @Data + public static class SettlementRow implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long settlementId; + private BigDecimal allocatedInvoiceAmount; + } + + @Data + public static class SheetRow implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private List lines; + } + + @Data + public static class LineRow implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private String goodsCategory; + private String goodsName; + private String unit; + private BigDecimal quantity; + private BigDecimal unitPriceNoTax; + private BigDecimal amountWithTax; + private BigDecimal taxRate; + private BigDecimal taxAmount; + private String remark; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationStatusRequest.java new file mode 100644 index 0000000..fa84d30 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceApplicationStatusRequest.java @@ -0,0 +1,43 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 开票申请状态请求 + * + * @author Chill + */ +@Data +public class InvoiceApplicationStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java new file mode 100644 index 0000000..e50ef5b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java @@ -0,0 +1,61 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 收票登记保存请求 + * + * @author Chill + */ +@Data +public class InvoiceReceiptSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long kingdeeInvoicePoolId; + private String phone; + private String customerEmails; + private String departmentEmails; + private String attachmentsJson; + private String remark; + private List settlements; + + /** + * 结算单分摊行 + */ + @Data + public static class SettlementRow implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long settlementId; + private BigDecimal allocatedInvoiceAmount; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptStatusRequest.java new file mode 100644 index 0000000..52cdc00 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptStatusRequest.java @@ -0,0 +1,43 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 收票登记状态请求 + * + * @author Chill + */ +@Data +public class InvoiceReceiptStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationInvoiceRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationInvoiceRequest.java new file mode 100644 index 0000000..3e6d8bb --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationInvoiceRequest.java @@ -0,0 +1,41 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 付款申请发票请求。 @author Chill */ +@Data +public class PaymentApplicationInvoiceRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private String settlementNo; + private String invoiceNo; + private LocalDate invoiceDate; + private String invoiceType; + private BigDecimal taxRate; + private BigDecimal invoiceAmount; + private BigDecimal matchedAmount; + private String attachmentJson; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java new file mode 100644 index 0000000..498de38 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java @@ -0,0 +1,61 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** 付款申请保存请求。 @author Chill */ +@Data +public class PaymentApplicationSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String paymentType; + private Long settlementId; + private Long preSettlementId; + private Long projectId; + private String projectName; + private Long deptId; + private String deptName; + private Long contractId; + private String contractNo; + private String contractName; + private String payerName; + private String payeeName; + private BigDecimal settlementAmount; + private BigDecimal payableAmount; + private String billType; + private BigDecimal paymentRatio; + private BigDecimal appliedAmount; + private String paymentMethod; + private Long billLedgerId; + private String billNo; + private Long receiptAccountId; + private String receiptAccountName; + private String bankName; + private String bankAccount; + private String attachmentsJson; + private String remark; + private List invoices; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationStatusRequest.java new file mode 100644 index 0000000..e351f75 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationStatusRequest.java @@ -0,0 +1,33 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** 付款申请状态请求。 @author Chill */ +@Data +public class PaymentApplicationStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimAttachmentsRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimAttachmentsRequest.java new file mode 100644 index 0000000..6bcb5dc --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimAttachmentsRequest.java @@ -0,0 +1,48 @@ +/** + * 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.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 认领记录附件维护请求 + * + * @author Chill + */ +@Data +@Schema(description = "认领记录附件维护请求") +public class ReceiptClaimAttachmentsRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long id; + private String attachmentsJson; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimRequest.java new file mode 100644 index 0000000..2039c76 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptClaimRequest.java @@ -0,0 +1,63 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 收款流水认领请求 + * + * @author Chill + */ +@Data +public class ReceiptClaimRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long flowId; + private String attachmentsJson; + private String remark; + private List settlements; + + /** + * 结算单分摊行 + */ + @Data + public static class SettlementRow implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long settlementId; + private BigDecimal allocatedReceiptAmount; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptFlowSyncRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptFlowSyncRequest.java new file mode 100644 index 0000000..fa443a7 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceiptFlowSyncRequest.java @@ -0,0 +1,69 @@ +/** + * 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.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 金蝶收款流水同步请求 + * + * @author Chill + */ +@Data +public class ReceiptFlowSyncRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private List flows; + + /** + * 金蝶收款流水行 + */ + @Data + public static class FlowRow implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String receiptNoticeNo; + private String payerName; + private BigDecimal receiptAmount; + private String counterpartyName; + private String counterpartyAccount; + private String counterpartyBank; + private String summary; + private LocalDateTime transactionTime; + private String detailSerialNo; + private LocalDateTime sourceUpdatedTime; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedger.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedger.java new file mode 100644 index 0000000..bb95358 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedger.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 汇票台账实体。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_bill_ledger") +public class BillLedger extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String billNo; + private Long issuerId; + private String issuerName; + private Long receiverId; + private String receiverName; + private String billType; + private BigDecimal faceAmount; + private BigDecimal availableBalance; + private LocalDate issueDate; + private LocalDate maturityDate; + private String availableDeptIdsJson; + private String availableDeptNames; + private Long feeBearerId; + private String feeBearerName; + private BigDecimal confirmedDiscountRate; + private String issuingBank; + private BigDecimal bankDiscountReferenceRate; + private BigDecimal estimatedDiscountFee; + private String attachmentsJson; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedgerUsage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedgerUsage.java new file mode 100644 index 0000000..b52c0a8 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillLedgerUsage.java @@ -0,0 +1,29 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** 汇票使用记录实体。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_bill_ledger_usage") +public class BillLedgerUsage extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long billLedgerId; + private Long paymentApplicationId; + private Long billPaymentId; + private String applicationNo; + private BigDecimal usedAmount; + private Long useDeptId; + private String useDeptName; + private String usageStatus; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillPayment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillPayment.java new file mode 100644 index 0000000..cf15e7f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/BillPayment.java @@ -0,0 +1,57 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 汇票付款实体。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_bill_payment") +public class BillPayment extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String paymentNo; + private Long billLedgerId; + private String billNo; + private BigDecimal faceAmount; + private BigDecimal availableBalance; + private BigDecimal usedAmount; + private Long deptId; + private String deptName; + private LocalDate paymentDate; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String attachmentsJson; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java new file mode 100644 index 0000000..bf03a3c --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java @@ -0,0 +1,83 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 开票申请实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_application") +@Schema(description = "开票申请") +public class InvoiceApplication extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String applicationNo; + private Long projectId; + private String projectName; + private Long deptId; + private String deptName; + private String issuerName; + private Long receiverCustomerId; + private String receiverName; + private String invoiceType; + private BigDecimal availableInvoiceAmount; + private BigDecimal invoiceAmount; + private LocalDate applicationDate; + private String applicantName; + private Long undertakingDeptId; + private String undertakingDeptName; + private String departmentEmails; + private Long receiverInvoiceInfoId; + private String taxpayerNo; + private String bankName; + private String bankAccount; + private String registeredAddress; + private String contactName; + private String contactPhone; + private String email; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String kingdeeBillNo; + private String kingdeeStatus; + private LocalDateTime syncedTime; + private String attachmentsJson; + private String remark; + private String voidReason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java new file mode 100644 index 0000000..4410493 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java @@ -0,0 +1,68 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 开票申请结算明细快照实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_application_detail") +public class InvoiceApplicationDetail extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long invoiceApplicationId; + private Long formalSettlementId; + private Long formalSettlementDetailId; + private Integer lineNo; + private String documentNo; + private String waybillNo; + private String vehicleNo; + private String departureAddress; + private String arrivalAddress; + private LocalDateTime actualDepartureTime; + private LocalDateTime actualCompletionTime; + private String transportType; + private String cargoName; + private String cargoType; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private String batchNo; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal settlementAmountTax; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java new file mode 100644 index 0000000..1615453 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java @@ -0,0 +1,58 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 开票申请商品行实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_application_line") +public class InvoiceApplicationLine extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long invoiceApplicationId; + private Long invoiceSheetId; + private Integer lineNo; + private String goodsCategory; + private String goodsName; + private String unit; + private BigDecimal quantity; + private BigDecimal unitPriceNoTax; + private BigDecimal amountWithTax; + private BigDecimal taxRate; + private BigDecimal taxAmount; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java new file mode 100644 index 0000000..8a411bd --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java @@ -0,0 +1,53 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** + * 开票申请操作记录实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_application_record") +public class InvoiceApplicationRecord extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long invoiceApplicationId; + private String actionType; + private String actionName; + private String fromStatus; + private String toStatus; + private String operatorName; + private String reason; + private String kingdeeBillNo; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java new file mode 100644 index 0000000..57a0339 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java @@ -0,0 +1,52 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 开票申请关联结算单实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_application_settlement") +public class InvoiceApplicationSettlement extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long invoiceApplicationId; + private Long formalSettlementId; + private String formalSettlementNo; + private BigDecimal settlementAmount; + private BigDecimal availableInvoiceAmount; + private BigDecimal allocatedInvoiceAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java new file mode 100644 index 0000000..8550946 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java @@ -0,0 +1,49 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 开票申请发票张次实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_application_sheet") +public class InvoiceApplicationSheet extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long invoiceApplicationId; + private Integer sheetNo; + private BigDecimal invoiceAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java new file mode 100644 index 0000000..67859e1 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java @@ -0,0 +1,83 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +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 lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 收票登记实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_receipt") +@Schema(description = "收票登记") +public class InvoiceReceipt extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + @JsonSerialize(using = ToStringSerializer.class) + private Long kingdeeInvoicePoolId; + private String invoiceNo; + private LocalDate invoiceDate; + private String invoiceType; + private BigDecimal taxRate; + private BigDecimal invoiceAmount; + private BigDecimal taxAmount; + private String receiverName; + private String issuerName; + @JsonSerialize(using = ToStringSerializer.class) + private Long projectId; + private String projectName; + @JsonSerialize(using = ToStringSerializer.class) + private Long deptId; + private String deptName; + private String payerName; + private String payeeName; + private String bankName; + private String bankAccount; + private String issuingBank; + private String phone; + private String customerEmails; + private String departmentEmails; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String kingdeeBillNo; + private String kingdeeStatus; + private String attachmentsJson; + private String remark; + private String voidReason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptRecord.java new file mode 100644 index 0000000..6e50246 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptRecord.java @@ -0,0 +1,58 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +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 lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** + * 收票登记操作记录实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_receipt_record") +@Schema(description = "收票登记操作记录") +public class InvoiceReceiptRecord extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + @JsonSerialize(using = ToStringSerializer.class) + private Long invoiceReceiptId; + private String actionType; + private String actionName; + private String fromStatus; + private String toStatus; + private String operatorName; + private String reason; + private String kingdeeBillNo; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java new file mode 100644 index 0000000..4f19d2d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java @@ -0,0 +1,58 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +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 lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 收票登记结算单分摊实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_invoice_receipt_settlement") +@Schema(description = "收票登记结算单分摊") +public class InvoiceReceiptSettlement extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + @JsonSerialize(using = ToStringSerializer.class) + private Long invoiceReceiptId; + @JsonSerialize(using = ToStringSerializer.class) + private Long formalSettlementId; + private String formalSettlementNo; + private BigDecimal settlementAmount; + private BigDecimal receivedInvoiceAmount; + private BigDecimal allocatedInvoiceAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java new file mode 100644 index 0000000..261110c --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java @@ -0,0 +1,68 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 金蝶进项发票票据池镜像实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_kingdee_invoice_pool") +@Schema(description = "金蝶进项发票票据池镜像") +public class KingdeeInvoicePool extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String invoiceNo; + private LocalDate invoiceDate; + private String invoiceType; + private BigDecimal taxRate; + private BigDecimal invoiceAmount; + private BigDecimal taxAmount; + private String receiverName; + private String issuerName; + private String bankName; + private String bankAccount; + private String issuingBank; + private String phone; + private String customerEmails; + private String departmentEmails; + private String kingdeeBillNo; + private String kingdeeStatus; + private String attachmentsJson; + private LocalDateTime sourceUpdatedTime; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java new file mode 100644 index 0000000..79b75c1 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java @@ -0,0 +1,64 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 金蝶收款流水镜像实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_kingdee_receipt_flow") +@Schema(description = "金蝶收款流水镜像") +public class KingdeeReceiptFlow extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + private String receiptNoticeNo; + private String payerName; + private BigDecimal receiptAmount; + private String counterpartyName; + private String counterpartyAccount; + private String counterpartyBank; + private String summary; + private LocalDateTime transactionTime; + private String detailSerialNo; + private BigDecimal claimedAmount; + private String claimStatus; + private LocalDateTime sourceUpdatedTime; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java new file mode 100644 index 0000000..1d6d033 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java @@ -0,0 +1,76 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 付款申请实体。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_payment_application") +public class PaymentApplication extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String paymentNo; + private String paymentType; + private Long settlementId; + private String settlementNo; + private Long preSettlementId; + private String preSettlementNo; + private Long projectId; + private String projectName; + private Long deptId; + private String deptName; + private Long contractId; + private String contractNo; + private String contractName; + private String payerName; + private String payeeName; + private BigDecimal settlementAmount; + private BigDecimal payableAmount; + private String billType; + private BigDecimal paymentRatio; + private BigDecimal appliedAmount; + private String paymentMethod; + private Long billLedgerId; + private String billNo; + private Long receiptAccountId; + private String receiptAccountName; + private String bankName; + private String bankAccount; + private String applicantName; + private LocalDate applyDate; + private String invoiceStatus; + private BigDecimal matchedInvoiceAmount; + private BigDecimal paidAmount; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String kingdeeBillNo; + private String kingdeeStatus; + private String attachmentsJson; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationInvoice.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationInvoice.java new file mode 100644 index 0000000..9a6ffdb --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationInvoice.java @@ -0,0 +1,47 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 付款申请发票明细。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_payment_application_invoice") +public class PaymentApplicationInvoice extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long paymentApplicationId; + private Integer lineNo; + private String settlementNo; + private String invoiceNo; + private LocalDate invoiceDate; + private String invoiceType; + private BigDecimal taxRate; + private BigDecimal invoiceAmount; + private BigDecimal matchedAmount; + private String attachmentJson; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java new file mode 100644 index 0000000..1c5c1af --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java @@ -0,0 +1,43 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 付款申请付款记录。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_payment_application_record") +public class PaymentApplicationRecord extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long paymentApplicationId; + private BigDecimal paidAmount; + private LocalDate paidDate; + private String paymentNo; + private String voucherJson; + private String kingdeeBillNo; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java new file mode 100644 index 0000000..fc7f786 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java @@ -0,0 +1,74 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +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 lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 收款流水认领实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_receipt_claim") +@Schema(description = "收款流水认领") +public class ReceiptClaim extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + private Long receiptFlowId; + private BigDecimal claimAmount; + @JsonSerialize(using = ToStringSerializer.class) + private Long claimerId; + private String claimerName; + @JsonSerialize(using = ToStringSerializer.class) + private Long claimerDeptId; + private String claimerDeptName; + private LocalDate claimDate; + private String attachmentsJson; + private String remark; + private String claimStatus; + private String kingdeeBillNo; + private String kingdeeBillStatus; + @JsonSerialize(using = ToStringSerializer.class) + private Long voidedBy; + private String voidedByName; + private LocalDateTime voidedTime; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimSettlement.java new file mode 100644 index 0000000..44df9c6 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimSettlement.java @@ -0,0 +1,63 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +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 lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 收款认领结算单分摊实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_receipt_claim_settlement") +@Schema(description = "收款认领结算单分摊") +public class ReceiptClaimSettlement extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + private Long receiptClaimId; + @JsonSerialize(using = ToStringSerializer.class) + private Long receiptFlowId; + @JsonSerialize(using = ToStringSerializer.class) + private Long formalSettlementId; + private String formalSettlementNo; + private BigDecimal settlementAmount; + private BigDecimal claimedReceiptAmount; + private BigDecimal allocatedReceiptAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptFlowRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptFlowRecord.java new file mode 100644 index 0000000..2fbe069 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptFlowRecord.java @@ -0,0 +1,64 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +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 lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 收款流水操作留痕实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_receipt_flow_record") +@Schema(description = "收款流水操作留痕") +public class ReceiptFlowRecord extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + private Long receiptFlowId; + @JsonSerialize(using = ToStringSerializer.class) + private Long receiptClaimId; + private String actionType; + private String actionName; + private String fromStatus; + private String toStatus; + private BigDecimal operationAmount; + private String operatorName; + private String content; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillLedgerVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillLedgerVO.java new file mode 100644 index 0000000..96cf8a4 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillLedgerVO.java @@ -0,0 +1,31 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.BillLedger; +import org.springblade.transport.pojo.entity.BillLedgerUsage; + +import java.io.Serial; +import java.time.LocalDate; +import java.util.List; + +/** 汇票台账视图。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class BillLedgerVO extends BillLedger { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private LocalDate issueStartDate; + @TableField(exist = false) private LocalDate issueEndDate; + @TableField(exist = false) private String maturityStatus; + @TableField(exist = false) private String expiryShortcut; + @TableField(exist = false) private String billTypeName; + @TableField(exist = false) private String maturityStatusName; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String updateUserName; + @TableField(exist = false) private List usageRecords; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillPaymentVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillPaymentVO.java new file mode 100644 index 0000000..9db1628 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BillPaymentVO.java @@ -0,0 +1,46 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.BillPayment; + +import java.io.Serial; +import java.time.LocalDate; + +/** 汇票付款视图。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class BillPaymentVO extends BillPayment { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private LocalDate paymentStartDate; + @TableField(exist = false) private LocalDate paymentEndDate; + @TableField(exist = false) private String approvalStatusName; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String updateUserName; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java new file mode 100644 index 0000000..0b0dd2f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java @@ -0,0 +1,47 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.InvoiceApplicationLine; +import org.springblade.transport.pojo.entity.InvoiceApplicationSheet; + +import java.io.Serial; +import java.util.List; + +/** + * 开票申请发票张次视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvoiceApplicationSheetVO extends InvoiceApplicationSheet { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private List lines; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java new file mode 100644 index 0000000..664d02b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java @@ -0,0 +1,56 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.InvoiceApplication; +import org.springblade.transport.pojo.entity.InvoiceApplicationDetail; +import org.springblade.transport.pojo.entity.InvoiceApplicationRecord; +import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement; + +import java.io.Serial; +import java.util.List; + +/** + * 开票申请视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InvoiceApplicationVO extends InvoiceApplication { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private String settlementNos; + @TableField(exist = false) private String approvalStatusName; + @TableField(exist = false) private String kingdeeStatusName; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private List settlements; + @TableField(exist = false) private List sheets; + @TableField(exist = false) private List details; + @TableField(exist = false) private List records; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java new file mode 100644 index 0000000..8be4dcf --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java @@ -0,0 +1,56 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.InvoiceReceipt; +import org.springblade.transport.pojo.entity.InvoiceReceiptRecord; +import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement; + +import java.io.Serial; +import java.util.List; + +/** + * 收票登记视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "收票登记视图实体类") +public class InvoiceReceiptVO extends InvoiceReceipt { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private String settlementNos; + @TableField(exist = false) private String approvalStatusName; + @TableField(exist = false) private String kingdeeStatusName; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String updateUserName; + @TableField(exist = false) private List settlements; + @TableField(exist = false) private List records; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java new file mode 100644 index 0000000..cbacc29 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java @@ -0,0 +1,47 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springblade.transport.pojo.entity.PaymentApplicationInvoice; +import org.springblade.transport.pojo.entity.PaymentApplicationRecord; + +import java.io.Serial; +import java.time.LocalDate; +import java.util.List; + +/** 付款申请视图。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PaymentApplicationVO extends PaymentApplication { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private LocalDate applyStartDate; + @TableField(exist = false) private LocalDate applyEndDate; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String updateUserName; + @TableField(exist = false) private String paymentTypeName; + @TableField(exist = false) private String approvalStatusName; + @TableField(exist = false) private String kingdeeStatusName; + @TableField(exist = false) private List invoices; + @TableField(exist = false) private List paymentRecords; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java new file mode 100644 index 0000000..9802b66 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java @@ -0,0 +1,93 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.ReceiptClaim; +import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 认领记录视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "认领记录视图实体类") +public class ReceiptClaimRecordVO extends ReceiptClaim { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField(exist = false) + private String receiptNoticeNo; + @TableField(exist = false) + private String payerName; + @TableField(exist = false) + private BigDecimal receiptAmount; + @TableField(exist = false) + private String counterpartyName; + @TableField(exist = false) + private String counterpartyAccount; + @TableField(exist = false) + private String counterpartyBank; + @TableField(exist = false) + private String summary; + @TableField(exist = false) + private LocalDateTime transactionTime; + @TableField(exist = false) + private String detailSerialNo; + @TableField(exist = false) + private String associatedSettlementNos; + @TableField(exist = false) + private String claimStatusName; + @TableField(exist = false) + private String kingdeeBillStatusName; + @TableField(exist = false) + private List settlements; + @TableField(exist = false) + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate transactionStartDate; + @TableField(exist = false) + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate transactionEndDate; + @TableField(exist = false) + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate claimStartDate; + @TableField(exist = false) + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate claimEndDate; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java new file mode 100644 index 0000000..caa11ce --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java @@ -0,0 +1,73 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 收款流水视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "收款流水视图实体类") +public class ReceiptFlowVO extends KingdeeReceiptFlow { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField(exist = false) + private String claimStatusName; + @TableField(exist = false) + private BigDecimal remainingAmount; + @TableField(exist = false) + private String createUserName; + @TableField(exist = false) + private String updateUserName; + @TableField(exist = false) + private String claimerName; + @TableField(exist = false) + private String claimerDeptName; + @TableField(exist = false) + private LocalDate claimDate; + @TableField(exist = false) + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime transactionStartTime; + @TableField(exist = false) + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime transactionEndTime; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java new file mode 100644 index 0000000..c699cdb --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java @@ -0,0 +1,82 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.BillLedgerSaveRequest; +import org.springblade.transport.pojo.vo.BillLedgerVO; +import org.springblade.transport.service.IBillLedgerService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** 汇票台账控制器。 @author Chill */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "bill_ledger") +@RequestMapping("/bill-ledger") +@Tag(name = "汇票台账", description = "汇票票据信息及可用余额管理") +public class BillLedgerController extends BladeController { + private final IBillLedgerService billLedgerService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "汇票台账分页") + public R> list(BillLedgerVO query, Query pageQuery) { + return R.data(billLedgerService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "汇票台账详情") + public R detail(@RequestParam Long id) { + return R.data(billLedgerService.detail(id)); + } + + @GetMapping("/expiry-counts") + @ApiOperationSupport(order = 3) + @Operation(summary = "汇票到期快捷统计") + public R> expiryCounts() { + return R.data(billLedgerService.expiryCounts()); + } + + @GetMapping("/available-options") + @ApiOperationSupport(order = 4) + @Operation(summary = "付款申请可用汇票") + public R> availableOptions(@RequestParam(required = false) String keyword, + @RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId) { + return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId)); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 5) + @Operation(summary = "新增或编辑汇票台账") + public R submit(@RequestBody BillLedgerSaveRequest request) { + return R.data(billLedgerService.submit(request)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 6) + @Operation(summary = "删除汇票台账") + public R remove(@RequestParam Long id) { + billLedgerService.removeLedger(id); + return R.success("删除成功"); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillPaymentController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillPaymentController.java new file mode 100644 index 0000000..3c1f3b4 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillPaymentController.java @@ -0,0 +1,118 @@ +/** + * 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.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.BillPaymentSaveRequest; +import org.springblade.transport.pojo.dto.BillPaymentStatusRequest; +import org.springblade.transport.pojo.vo.BillPaymentVO; +import org.springblade.transport.service.IBillPaymentService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** 汇票付款控制器。 @author Chill */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "bill_payment") +@RequestMapping("/bill-payment") +@Tag(name = "汇票付款", description = "汇票付款单据管理") +public class BillPaymentController extends BladeController { + private final IBillPaymentService billPaymentService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "汇票付款分页") + public R> list(BillPaymentVO query, Query pageQuery) { + return R.data(billPaymentService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "汇票付款详情") + public R detail(@RequestParam Long id) { + return R.data(billPaymentService.detail(id)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 3) + @Operation(summary = "保存汇票付款") + public R save(@RequestBody BillPaymentSaveRequest request) { + return R.data(billPaymentService.saveDraft(request)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 4) + @Operation(summary = "删除汇票付款草稿") + public R remove(@RequestParam Long id) { + billPaymentService.removeDraft(id); + return R.success("删除成功"); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 5) + @Operation(summary = "提交汇票付款") + public R submit(@RequestBody BillPaymentStatusRequest request) { + billPaymentService.submit(request); + return R.success("提交成功"); + } + + @PostMapping("/approve") + @ApiOperationSupport(order = 6) + @Operation(summary = "审批通过汇票付款") + public R approve(@RequestBody BillPaymentStatusRequest request) { + billPaymentService.approve(request); + return R.success("审批通过"); + } + + @PostMapping("/return") + @ApiOperationSupport(order = 7) + @Operation(summary = "驳回汇票付款") + public R returnBill(@RequestBody BillPaymentStatusRequest request) { + billPaymentService.returnBill(request); + return R.success("已驳回"); + } + + @PostMapping("/void") + @ApiOperationSupport(order = 8) + @Operation(summary = "作废汇票付款") + public R voidBill(@RequestBody BillPaymentStatusRequest request) { + billPaymentService.voidBill(request); + return R.success("作废成功"); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java new file mode 100644 index 0000000..35ddac0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java @@ -0,0 +1,154 @@ +/** + * 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.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest; +import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.vo.InvoiceApplicationVO; +import org.springblade.transport.service.IInvoiceApplicationService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * 开票申请控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "invoice_application") +@RequestMapping("/invoice-application") +@Tag(name = "开票管理", description = "开票申请管理") +public class InvoiceApplicationController extends BladeController { + private final IInvoiceApplicationService invoiceApplicationService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "开票申请分页") + public R> list(InvoiceApplicationVO query, Query pageQuery) { + return R.data(invoiceApplicationService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "开票申请详情") + public R detail(@RequestParam Long id) { + return R.data(invoiceApplicationService.detail(id)); + } + + @GetMapping("/settlement-candidates") + @ApiOperationSupport(order = 3) + @Operation(summary = "可开票正式结算单") + public R>> settlementCandidates(@RequestParam(required = false) String keyword) { + return R.data(invoiceApplicationService.settlementCandidates(keyword)); + } + + @GetMapping("/settlement-details") + @ApiOperationSupport(order = 4) + @Operation(summary = "结算单可选明细") + public R> settlementDetails(@RequestParam String settlementIds) { + return R.data(invoiceApplicationService.settlementDetails(settlementIds)); + } + + @GetMapping("/receiver-information") + @ApiOperationSupport(order = 5) + @Operation(summary = "受票方开票信息") + public R> receiverInformation(@RequestParam String settlementIds) { + return R.data(invoiceApplicationService.receiverInformation(settlementIds)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 6) + @Operation(summary = "保存开票申请") + public R save(@RequestBody InvoiceApplicationSaveRequest request) { + return R.data(invoiceApplicationService.saveDraft(request)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 7) + @Operation(summary = "删除开票申请草稿") + public R remove(@RequestParam Long id) { + invoiceApplicationService.removeDraft(id); + return R.success("删除成功"); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 8) + @Operation(summary = "提交开票申请") + public R submit(@RequestBody InvoiceApplicationStatusRequest request) { + invoiceApplicationService.submit(request); + return R.success("提交成功"); + } + + @PostMapping("/approve") + @ApiOperationSupport(order = 9) + @Operation(summary = "审批通过开票申请") + public R approve(@RequestBody InvoiceApplicationStatusRequest request) { + invoiceApplicationService.approve(request); + return R.success("审批通过"); + } + + @PostMapping("/return") + @ApiOperationSupport(order = 10) + @Operation(summary = "驳回开票申请") + public R returnBill(@RequestBody InvoiceApplicationStatusRequest request) { + invoiceApplicationService.returnBill(request); + return R.success("已驳回"); + } + + @PostMapping("/void") + @ApiOperationSupport(order = 11) + @Operation(summary = "作废开票申请") + public R voidBill(@RequestBody InvoiceApplicationStatusRequest request) { + invoiceApplicationService.voidBill(request); + return R.success("作废成功"); + } + + @PostMapping("/sync-kingdee") + @ApiOperationSupport(order = 12) + @Operation(summary = "同步金蝶开票申请") + public R syncKingdee(@RequestParam Long id) { + return R.data(invoiceApplicationService.syncKingdee(id)); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java new file mode 100644 index 0000000..1a01d3c --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java @@ -0,0 +1,157 @@ +/** + * 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.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest; +import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest; +import org.springblade.transport.pojo.entity.KingdeeInvoicePool; +import org.springblade.transport.pojo.vo.InvoiceReceiptVO; +import org.springblade.transport.service.IInvoiceReceiptService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * 收票登记控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "invoice_receipt") +@RequestMapping("/invoice-receipt") +@Tag(name = "收票管理", description = "进项发票登记认领管理") +public class InvoiceReceiptController extends BladeController { + + private final IInvoiceReceiptService invoiceReceiptService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "收票登记分页") + public R> list(InvoiceReceiptVO query, Query pageQuery) { + return R.data(invoiceReceiptService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "收票登记详情") + public R detail(@RequestParam Long id) { + return R.data(invoiceReceiptService.detail(id)); + } + + @GetMapping("/invoice-pool") + @ApiOperationSupport(order = 3) + @Operation(summary = "查询金蝶进项发票票据池") + public R> invoicePool(@RequestParam(required = false) String keyword) { + return R.data(invoiceReceiptService.invoicePool(keyword)); + } + + @GetMapping("/settlement-candidates") + @ApiOperationSupport(order = 4) + @Operation(summary = "可关联的应付正式结算单") + public R>> settlementCandidates( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long receiptId) { + return R.data(invoiceReceiptService.settlementCandidates(keyword, receiptId)); + } + + @GetMapping("/reference-information") + @ApiOperationSupport(order = 5) + @Operation(summary = "收票关联参考信息") + public R> referenceInformation(@RequestParam String settlementIds) { + return R.data(invoiceReceiptService.referenceInformation(settlementIds)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 6) + @Operation(summary = "保存收票登记") + public R save(@RequestBody InvoiceReceiptSaveRequest request) { + return R.data(invoiceReceiptService.saveDraft(request)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 7) + @Operation(summary = "删除收票登记草稿") + public R remove(@RequestParam Long id) { + invoiceReceiptService.removeDraft(id); + return R.success("删除成功"); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 8) + @Operation(summary = "提交收票登记") + public R submit(@RequestBody InvoiceReceiptStatusRequest request) { + invoiceReceiptService.submit(request); + return R.success("提交成功"); + } + + @PostMapping("/approve") + @ApiOperationSupport(order = 9) + @Operation(summary = "审批通过收票登记") + public R approve(@RequestBody InvoiceReceiptStatusRequest request) { + invoiceReceiptService.approve(request); + return R.success("审批通过"); + } + + @PostMapping("/return") + @ApiOperationSupport(order = 10) + @Operation(summary = "驳回收票登记") + public R returnBill(@RequestBody InvoiceReceiptStatusRequest request) { + invoiceReceiptService.returnBill(request); + return R.success("已驳回"); + } + + @PostMapping("/void") + @ApiOperationSupport(order = 11) + @Operation(summary = "作废收票登记") + public R voidBill(@RequestBody InvoiceReceiptStatusRequest request) { + invoiceReceiptService.voidBill(request); + return R.success("作废成功"); + } + + @PostMapping("/sync-kingdee") + @ApiOperationSupport(order = 12) + @Operation(summary = "同步金蝶发票状态") + public R syncKingdee(@RequestParam Long id) { + return R.data(invoiceReceiptService.syncKingdee(id)); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java new file mode 100644 index 0000000..575bf17 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -0,0 +1,87 @@ +/** + * 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.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; +import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; +import org.springblade.transport.pojo.vo.PaymentApplicationVO; +import org.springblade.transport.service.IPaymentApplicationService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** 付款申请控制器。 @author Chill */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "payment_application") +@RequestMapping("/payment-application") +@Tag(name = "付款管理", description = "付款申请管理") +public class PaymentApplicationController extends BladeController { + private final IPaymentApplicationService paymentApplicationService; + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "付款申请分页") + public R> list(PaymentApplicationVO query, Query pageQuery) { return R.data(paymentApplicationService.selectPage(Condition.getPage(pageQuery), query)); } + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "付款申请详情") + public R detail(@RequestParam Long id) { return R.data(paymentApplicationService.detail(id)); } + @PostMapping("/save") + @ApiOperationSupport(order = 3) + @Operation(summary = "保存付款申请") + public R save(@RequestBody PaymentApplicationSaveRequest request) { return R.data(paymentApplicationService.saveDraft(request)); } + @PostMapping("/remove") + @ApiOperationSupport(order = 4) + @Operation(summary = "删除付款申请草稿") + public R remove(@RequestParam Long id) { paymentApplicationService.removeDraft(id); return R.success("删除成功"); } + @PostMapping("/submit") + @ApiOperationSupport(order = 5) + @Operation(summary = "提交付款申请") + public R submit(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.submit(request); return R.success("提交成功"); } + @PostMapping("/approve") + @ApiOperationSupport(order = 6) + @Operation(summary = "审批通过付款申请") + public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); return R.success("审批通过"); } + @PostMapping("/return") + @ApiOperationSupport(order = 7) + @Operation(summary = "驳回付款申请") + public R returnBill(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.returnBill(request); return R.success("已驳回"); } + @PostMapping("/void") + @ApiOperationSupport(order = 8) + @Operation(summary = "作废付款申请") + public R voidBill(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.voidBill(request); return R.success("作废成功"); } + @PostMapping("/sync-kingdee") + @ApiOperationSupport(order = 9) + @Operation(summary = "生成金蝶付款单") + public R syncKingdee(@RequestParam Long id) { return R.data(paymentApplicationService.syncKingdee(id)); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java new file mode 100644 index 0000000..7cfd3f8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java @@ -0,0 +1,90 @@ +/** + * 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.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.ReceiptClaimAttachmentsRequest; +import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO; +import org.springblade.transport.service.IReceiptClaimRecordService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 认领记录控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "receipt_claim_record") +@RequestMapping("/receipt-claim-record") +@Tag(name = "认领记录", description = "当前用户收款认领记录查询与作废") +public class ReceiptClaimRecordController extends BladeController { + + private final IReceiptClaimRecordService receiptClaimRecordService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "当前用户认领记录分页") + public R> list(ReceiptClaimRecordVO query, Query pageQuery) { + return R.data(receiptClaimRecordService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "认领记录详情") + public R detail(@RequestParam Long id) { + return R.data(receiptClaimRecordService.detail(id)); + } + + @PostMapping("/attachments") + @ApiOperationSupport(order = 3) + @Operation(summary = "维护本人认领记录附件") + public R updateAttachments(@RequestBody ReceiptClaimAttachmentsRequest request) { + receiptClaimRecordService.updateAttachments(request); + return R.success(); + } + + @PostMapping("/void") + @ApiOperationSupport(order = 4) + @Operation(summary = "作废认领记录并生成金蝶认领冲单") + public R voidClaim(@RequestParam Long id) { + return R.data(receiptClaimRecordService.voidClaim(id)); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java new file mode 100644 index 0000000..4aa3f02 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java @@ -0,0 +1,102 @@ +/** + * 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.transport.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 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.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.ReceiptClaimRequest; +import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest; +import org.springblade.transport.pojo.vo.ReceiptFlowVO; +import org.springblade.transport.service.IReceiptFlowService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * 收款流水控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "receipt_flow") +@RequestMapping("/receipt-flow") +@Tag(name = "收款流水", description = "金蝶收款流水同步与认领管理") +public class ReceiptFlowController extends BladeController { + + private final IReceiptFlowService receiptFlowService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "收款流水分页") + public R> list(ReceiptFlowVO query, Query pageQuery) { + return R.data(receiptFlowService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "收款流水详情") + public R detail(@RequestParam Long id) { + return R.data(receiptFlowService.detail(id)); + } + + @GetMapping("/settlement-candidates") + @ApiOperationSupport(order = 3) + @Operation(summary = "可关联的应收正式结算单") + public R>> settlementCandidates( + @RequestParam(required = false) String keyword, + @RequestParam Long flowId) { + return R.data(receiptFlowService.settlementCandidates(keyword, flowId)); + } + + @PostMapping("/claim") + @ApiOperationSupport(order = 4) + @Operation(summary = "认领收款流水") + public R claim(@RequestBody ReceiptClaimRequest request) { + return R.data(receiptFlowService.claim(request)); + } + + @PostMapping("/sync") + @ApiOperationSupport(order = 5) + @Operation(summary = "手动同步金蝶收款流水") + public R sync(@RequestBody(required = false) ReceiptFlowSyncRequest request) { + return R.data(receiptFlowService.sync(request)); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerMapper.java new file mode 100644 index 0000000..15a7948 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerMapper.java @@ -0,0 +1,14 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.BillLedger; + +/** 汇票台账 Mapper。 @author Chill */ +@Mapper +public interface BillLedgerMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerUsageMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerUsageMapper.java new file mode 100644 index 0000000..49915d1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillLedgerUsageMapper.java @@ -0,0 +1,14 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.BillLedgerUsage; + +/** 汇票使用记录 Mapper。 @author Chill */ +@Mapper +public interface BillLedgerUsageMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillPaymentMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillPaymentMapper.java new file mode 100644 index 0000000..2b2db97 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/BillPaymentMapper.java @@ -0,0 +1,35 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.BillPayment; + +/** 汇票付款 Mapper。 @author Chill */ +@Mapper +public interface BillPaymentMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationDetailMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationDetailMapper.java new file mode 100644 index 0000000..bdfc170 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationDetailMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceApplicationDetail; + +/** + * 开票申请结算明细 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceApplicationDetailMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationLineMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationLineMapper.java new file mode 100644 index 0000000..412052f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationLineMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceApplicationLine; + +/** + * 开票申请商品行 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceApplicationLineMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationMapper.java new file mode 100644 index 0000000..9e9db78 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceApplication; + +/** + * 开票申请 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceApplicationMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationRecordMapper.java new file mode 100644 index 0000000..b3666bc --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationRecordMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceApplicationRecord; + +/** + * 开票申请操作记录 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceApplicationRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSettlementMapper.java new file mode 100644 index 0000000..1cc35ba --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSettlementMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement; + +/** + * 开票申请结算单 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceApplicationSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSheetMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSheetMapper.java new file mode 100644 index 0000000..6715132 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceApplicationSheetMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceApplicationSheet; + +/** + * 开票申请发票张次 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceApplicationSheetMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptMapper.java new file mode 100644 index 0000000..eb554ae --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceReceipt; + +/** + * 收票登记 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceReceiptMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptRecordMapper.java new file mode 100644 index 0000000..bd7501f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptRecordMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceReceiptRecord; + +/** + * 收票登记操作记录 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceReceiptRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptSettlementMapper.java new file mode 100644 index 0000000..3a26ea7 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InvoiceReceiptSettlementMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement; + +/** + * 收票登记结算单分摊 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface InvoiceReceiptSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java new file mode 100644 index 0000000..d299c63 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.KingdeeInvoicePool; + +/** + * 金蝶进项发票票据池 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface KingdeeInvoicePoolMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java new file mode 100644 index 0000000..7d69772 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; + +/** + * 金蝶收款流水镜像 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface KingdeeReceiptFlowMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationInvoiceMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationInvoiceMapper.java new file mode 100644 index 0000000..21c8c0a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationInvoiceMapper.java @@ -0,0 +1,29 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PaymentApplicationInvoice; + +/** 付款申请发票 Mapper。 @author Chill */ +@Mapper +public interface PaymentApplicationInvoiceMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationMapper.java new file mode 100644 index 0000000..770dc20 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationMapper.java @@ -0,0 +1,29 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PaymentApplication; + +/** 付款申请 Mapper。 @author Chill */ +@Mapper +public interface PaymentApplicationMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationRecordMapper.java new file mode 100644 index 0000000..dd9dfb8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationRecordMapper.java @@ -0,0 +1,29 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PaymentApplicationRecord; + +/** 付款申请付款记录 Mapper。 @author Chill */ +@Mapper +public interface PaymentApplicationRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.java new file mode 100644 index 0000000..cf0fd17 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.java @@ -0,0 +1,50 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springblade.transport.pojo.entity.ReceiptClaim; +import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO; + +import java.util.List; + +/** + * 收款流水认领 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface ReceiptClaimMapper extends BaseMapper { + + List selectClaimRecordPage(IPage page, + @Param("query") ReceiptClaimRecordVO query, @Param("claimerId") Long claimerId); + + ReceiptClaimRecordVO selectClaimRecordDetail(@Param("id") Long id, + @Param("claimerId") Long claimerId); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml new file mode 100644 index 0000000..07a2553 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimMapper.xml @@ -0,0 +1,114 @@ + + + + + + c.id, + c.tenant_id, + c.create_user, + c.create_dept, + c.create_time, + c.update_user, + c.update_time, + c.status, + c.is_deleted, + c.receipt_flow_id, + c.claim_amount, + c.claimer_id, + c.claimer_name, + c.claimer_dept_id, + c.claimer_dept_name, + c.claim_date, + c.attachments_json, + c.remark, + c.claim_status, + c.kingdee_bill_no, + c.kingdee_bill_status, + c.voided_by, + c.voided_by_name, + c.voided_time, + f.receipt_notice_no, + f.payer_name, + f.receipt_amount, + f.counterparty_name, + f.counterparty_account, + f.counterparty_bank, + f.summary, + f.transaction_time, + f.detail_serial_no, + (SELECT GROUP_CONCAT(rcs.formal_settlement_no ORDER BY rcs.id SEPARATOR ',') + FROM blade_receipt_claim_settlement rcs + WHERE rcs.receipt_claim_id = c.id AND rcs.is_deleted = 0) AS associated_settlement_nos + + + + c.is_deleted = 0 + AND f.is_deleted = 0 + AND c.claimer_id = #{claimerId} + + + AND f.receipt_notice_no LIKE #{receiptNoticeNoLike} + + + + AND f.counterparty_name LIKE #{counterpartyNameLike} + + + + AND f.counterparty_bank LIKE #{counterpartyBankLike} + + + + AND f.counterparty_account LIKE #{counterpartyAccountLike} + + + + AND f.summary LIKE #{summaryLike} + + + AND c.claim_status = #{query.claimStatus} + + + AND f.transaction_time >= #{query.transactionStartDate} + + + AND f.transaction_time < DATE_ADD(#{query.transactionEndDate}, INTERVAL 1 DAY) + + + + AND c.claimer_name LIKE #{claimerNameLike} + + + AND c.claim_date >= #{query.claimStartDate} + + + AND c.claim_date <= #{query.claimEndDate} + + + + AND c.claimer_dept_name LIKE #{claimerDeptNameLike} + + + AND c.kingdee_bill_status = #{query.kingdeeBillStatus} + + + + + + + + diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimSettlementMapper.java new file mode 100644 index 0000000..3537992 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimSettlementMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; + +/** + * 收款认领结算单分摊 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface ReceiptClaimSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptFlowRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptFlowRecordMapper.java new file mode 100644 index 0000000..2f05829 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptFlowRecordMapper.java @@ -0,0 +1,39 @@ +/** + * 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.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.ReceiptFlowRecord; + +/** + * 收款流水操作留痕 Mapper 接口 + * + * @author Chill + */ +@Mapper +public interface ReceiptFlowRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java new file mode 100644 index 0000000..c31abd0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java @@ -0,0 +1,24 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.BillLedgerSaveRequest; +import org.springblade.transport.pojo.entity.BillLedger; +import org.springblade.transport.pojo.vo.BillLedgerVO; + +import java.util.List; +import java.util.Map; + +/** 汇票台账服务。 @author Chill */ +public interface IBillLedgerService extends BaseService { + IPage selectPage(IPage page, BillLedgerVO query); + BillLedgerVO detail(Long id); + Map expiryCounts(); + List availableOptions(String keyword, Long deptId, Long selectedId); + Long submit(BillLedgerSaveRequest request); + void removeLedger(Long id); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillPaymentService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillPaymentService.java new file mode 100644 index 0000000..089d064 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillPaymentService.java @@ -0,0 +1,45 @@ +/** + * 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.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.BillPaymentSaveRequest; +import org.springblade.transport.pojo.dto.BillPaymentStatusRequest; +import org.springblade.transport.pojo.entity.BillPayment; +import org.springblade.transport.pojo.vo.BillPaymentVO; + +/** 汇票付款服务。 @author Chill */ +public interface IBillPaymentService extends BaseService { + IPage selectPage(IPage page, BillPaymentVO query); + BillPaymentVO detail(Long id); + Long saveDraft(BillPaymentSaveRequest request); + void removeDraft(Long id); + void submit(BillPaymentStatusRequest request); + void approve(BillPaymentStatusRequest request); + void returnBill(BillPaymentStatusRequest request); + void voidBill(BillPaymentStatusRequest request); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java new file mode 100644 index 0000000..fe65e70 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java @@ -0,0 +1,57 @@ +/** + * 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.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest; +import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.InvoiceApplication; +import org.springblade.transport.pojo.vo.InvoiceApplicationVO; + +import java.util.List; +import java.util.Map; + +/** + * 开票申请服务 + * + * @author Chill + */ +public interface IInvoiceApplicationService extends BaseService { + IPage selectPage(IPage page, InvoiceApplicationVO query); + InvoiceApplicationVO detail(Long id); + List> settlementCandidates(String keyword); + List settlementDetails(String settlementIds); + Map receiverInformation(String settlementIds); + Long saveDraft(InvoiceApplicationSaveRequest request); + void removeDraft(Long id); + void submit(InvoiceApplicationStatusRequest request); + void approve(InvoiceApplicationStatusRequest request); + void returnBill(InvoiceApplicationStatusRequest request); + void voidBill(InvoiceApplicationStatusRequest request); + String syncKingdee(Long id); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java new file mode 100644 index 0000000..c6253ae --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java @@ -0,0 +1,69 @@ +/** + * 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.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest; +import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest; +import org.springblade.transport.pojo.entity.InvoiceReceipt; +import org.springblade.transport.pojo.entity.KingdeeInvoicePool; +import org.springblade.transport.pojo.vo.InvoiceReceiptVO; + +import java.util.List; +import java.util.Map; + +/** + * 收票登记服务 + * + * @author Chill + */ +public interface IInvoiceReceiptService extends BaseService { + + IPage selectPage(IPage page, InvoiceReceiptVO query); + + InvoiceReceiptVO detail(Long id); + + List invoicePool(String keyword); + + List> settlementCandidates(String keyword, Long receiptId); + + Map referenceInformation(String settlementIds); + + Long saveDraft(InvoiceReceiptSaveRequest request); + + void removeDraft(Long id); + + void submit(InvoiceReceiptStatusRequest request); + + void approve(InvoiceReceiptStatusRequest request); + + void returnBill(InvoiceReceiptStatusRequest request); + + void voidBill(InvoiceReceiptStatusRequest request); + + String syncKingdee(Long id); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java new file mode 100644 index 0000000..7a9c0f4 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java @@ -0,0 +1,40 @@ +/** + * 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.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; +import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; +import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springblade.transport.pojo.vo.PaymentApplicationVO; + +/** 付款申请服务。 @author Chill */ +public interface IPaymentApplicationService extends BaseService { + IPage selectPage(IPage page, PaymentApplicationVO query); + PaymentApplicationVO detail(Long id); + Long saveDraft(PaymentApplicationSaveRequest request); + void removeDraft(Long id); + void submit(PaymentApplicationStatusRequest request); + void approve(PaymentApplicationStatusRequest request); + void returnBill(PaymentApplicationStatusRequest request); + void voidBill(PaymentApplicationStatusRequest request); + String syncKingdee(Long id); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java new file mode 100644 index 0000000..66a8886 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java @@ -0,0 +1,49 @@ +/** + * 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.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.ReceiptClaimAttachmentsRequest; +import org.springblade.transport.pojo.entity.ReceiptClaim; +import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO; + +/** + * 认领记录服务 + * + * @author Chill + */ +public interface IReceiptClaimRecordService extends BaseService { + + IPage selectPage(IPage page, + ReceiptClaimRecordVO query); + + ReceiptClaimRecordVO detail(Long id); + + void updateAttachments(ReceiptClaimAttachmentsRequest request); + + String voidClaim(Long id); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java new file mode 100644 index 0000000..816990d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java @@ -0,0 +1,54 @@ +/** + * 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.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.ReceiptClaimRequest; +import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springblade.transport.pojo.vo.ReceiptFlowVO; + +import java.util.List; +import java.util.Map; + +/** + * 收款流水服务 + * + * @author Chill + */ +public interface IReceiptFlowService extends BaseService { + + IPage selectPage(IPage page, ReceiptFlowVO query); + + ReceiptFlowVO detail(Long id); + + List> settlementCandidates(String keyword, Long flowId); + + Long claim(ReceiptClaimRequest request); + + int sync(ReceiptFlowSyncRequest request); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java new file mode 100644 index 0000000..e3645c2 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java @@ -0,0 +1,302 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.BillLedgerMapper; +import org.springblade.transport.mapper.BillLedgerUsageMapper; +import org.springblade.transport.mapper.CustomerArchiveMapper; +import org.springblade.transport.pojo.dto.BillLedgerSaveRequest; +import org.springblade.transport.pojo.entity.BillLedger; +import org.springblade.transport.pojo.entity.BillLedgerUsage; +import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.vo.BillLedgerVO; +import org.springblade.transport.service.IBillLedgerService; +import org.springblade.transport.wrapper.BillLedgerWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** 汇票台账服务实现。 @author Chill */ +@Service +@RequiredArgsConstructor +public class BillLedgerServiceImpl extends BaseServiceImpl + implements IBillLedgerService { + private static final String APPROVED = "approved"; + private final BillLedgerUsageMapper usageMapper; + private final CustomerArchiveMapper customerArchiveMapper; + + @Override + public IPage selectPage(IPage page, BillLedgerVO query) { + LocalDate today = LocalDate.now(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getBillNo()), BillLedger::getBillNo, query.getBillNo()) + .ge(query.getIssueStartDate() != null, BillLedger::getIssueDate, query.getIssueStartDate()) + .le(query.getIssueEndDate() != null, BillLedger::getIssueDate, query.getIssueEndDate()) + .like(Func.isNotEmpty(query.getIssuerName()), BillLedger::getIssuerName, query.getIssuerName()) + .like(Func.isNotEmpty(query.getReceiverName()), BillLedger::getReceiverName, query.getReceiverName()) + .eq(Func.isNotEmpty(query.getBillType()), BillLedger::getBillType, query.getBillType()); + applyMaturityStatus(wrapper, query.getMaturityStatus(), today); + applyExpiryShortcut(wrapper, query.getExpiryShortcut(), today); + wrapper.orderByDesc(BillLedger::getCreateTime); + return page(page, wrapper).convert(item -> BillLedgerWrapper.build().entityVO(item)); + } + + @Override + public BillLedgerVO detail(Long id) { + BillLedgerVO vo = BillLedgerWrapper.build().entityVO(existing(id)); + vo.setUsageRecords(usageMapper.selectList(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getBillLedgerId, id) + .eq(BillLedgerUsage::getUsageStatus, APPROVED) + .orderByDesc(BillLedgerUsage::getCreateTime))); + return vo; + } + + @Override + public Map expiryCounts() { + LocalDate today = LocalDate.now(); + Map counts = new LinkedHashMap<>(); + counts.put("all", count()); + counts.put("within30", count(Wrappers.lambdaQuery() + .ge(BillLedger::getMaturityDate, today) + .le(BillLedger::getMaturityDate, today.plusDays(30)))); + counts.put("within90", count(Wrappers.lambdaQuery() + .gt(BillLedger::getMaturityDate, today.plusDays(30)) + .le(BillLedger::getMaturityDate, today.plusDays(90)))); + counts.put("over90", count(Wrappers.lambdaQuery() + .gt(BillLedger::getMaturityDate, today.plusDays(90)))); + return counts; + } + + @Override + public List availableOptions(String keyword, Long deptId, Long selectedId) { + LocalDate today = LocalDate.now(); + return list(Wrappers.lambdaQuery() + .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword) + .or().like(BillLedger::getIssuerName, keyword) + .or().like(BillLedger::getReceiverName, keyword)) + .and(wrapper -> wrapper + .gt(BillLedger::getAvailableBalance, BigDecimal.ZERO) + .ge(BillLedger::getMaturityDate, today) + .or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId))) + .orderByAsc(BillLedger::getMaturityDate) + .orderByDesc(BillLedger::getCreateTime) + .last("limit 200")).stream() + .filter(item -> selectedId != null && Objects.equals(item.getId(), selectedId) + || departmentAvailable(item, deptId)) + .map(item -> BillLedgerWrapper.build().entityVO(item)) + .toList(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long submit(BillLedgerSaveRequest request) { + validateRequest(request); + BillLedger entity = request.getId() == null ? new BillLedger() : locked(request.getId()); + String billNo = required(request.getBillNo(), "票据号码", 32); + if (entity.getId() != null && !billNo.equals(entity.getBillNo())) { + throw new ServiceException("票据号码编辑时不可修改"); + } + Long duplicate = count(Wrappers.lambdaQuery() + .eq(BillLedger::getBillNo, billNo) + .ne(entity.getId() != null, BillLedger::getId, entity.getId())); + if (duplicate > 0) { + throw new ServiceException("票据号码已存在"); + } + CustomerArchive issuer = customer(request.getIssuerId(), "出票单位"); + CustomerArchive feeBearer = customer(request.getFeeBearerId(), "费用承担方"); + BigDecimal faceAmount = positive(request.getFaceAmount(), "票面金额").setScale(2, RoundingMode.HALF_UP); + BigDecimal usedAmount = entity.getId() == null ? BigDecimal.ZERO : activeUsedAmount(entity.getId()); + if (faceAmount.compareTo(usedAmount) < 0) { + throw new ServiceException("票面金额不能小于已使用金额"); + } + + entity.setBillNo(billNo); + entity.setIssuerId(issuer.getId()); + entity.setIssuerName(customerName(issuer)); + entity.setReceiverName(required(request.getReceiverName(), "收票单位", 100)); + entity.setBillType(request.getBillType()); + entity.setFaceAmount(faceAmount); + entity.setAvailableBalance(faceAmount.subtract(usedAmount).setScale(2, RoundingMode.HALF_UP)); + entity.setIssueDate(request.getIssueDate()); + entity.setMaturityDate(request.getMaturityDate()); + entity.setAvailableDeptIdsJson(request.getAvailableDeptIdsJson()); + entity.setAvailableDeptNames(required(request.getAvailableDeptNames(), "可用部门", 500)); + entity.setFeeBearerId(feeBearer.getId()); + entity.setFeeBearerName(customerName(feeBearer)); + entity.setConfirmedDiscountRate(rate(request.getConfirmedDiscountRate(), "双方确认贴现率")); + entity.setIssuingBank(required(request.getIssuingBank(), "出票行", 100)); + entity.setBankDiscountReferenceRate(rate(request.getBankDiscountReferenceRate(), "银行贴现参考率")); + entity.setEstimatedDiscountFee(calculateDiscountFee(faceAmount, entity.getConfirmedDiscountRate())); + entity.setAttachmentsJson(request.getAttachmentsJson()); + entity.setRemark(limit(request.getRemark(), 200, "备注")); + if (entity.getId() == null) { + entity.setStatus(1); + } + saveOrUpdate(entity); + return entity.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeLedger(Long id) { + BillLedger entity = locked(id); + if (usageMapper.selectCount(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getBillLedgerId, id) + .eq(BillLedgerUsage::getUsageStatus, APPROVED)) > 0) { + throw new ServiceException("存在已审核使用记录的汇票不允许删除"); + } + removeById(entity); + } + + private void validateRequest(BillLedgerSaveRequest request) { + if (request == null) { + throw new ServiceException("请求参数不能为空"); + } + if (!List.of("issued", "received").contains(request.getBillType())) { + throw new ServiceException("汇票类型不合法"); + } + if (request.getIssueDate() == null) { + throw new ServiceException("出票日期不能为空"); + } + if (request.getMaturityDate() == null || !request.getMaturityDate().isAfter(request.getIssueDate())) { + throw new ServiceException("到期日期必须晚于出票日期"); + } + List deptIds = parseArray(request.getAvailableDeptIdsJson(), "可用部门"); + if (deptIds.isEmpty()) { + throw new ServiceException("可用部门不能为空"); + } + } + + private void applyMaturityStatus(LambdaQueryWrapper wrapper, String status, + LocalDate today) { + if (Func.isEmpty(status)) return; + switch (status) { + case "expired" -> wrapper.lt(BillLedger::getMaturityDate, today); + case "due_today" -> wrapper.eq(BillLedger::getMaturityDate, today); + case "unexpired" -> wrapper.gt(BillLedger::getMaturityDate, today); + default -> throw new ServiceException("到期状态不合法"); + } + } + + private void applyExpiryShortcut(LambdaQueryWrapper wrapper, String shortcut, + LocalDate today) { + if (Func.isEmpty(shortcut) || "all".equals(shortcut)) return; + switch (shortcut) { + case "within30" -> wrapper.ge(BillLedger::getMaturityDate, today) + .le(BillLedger::getMaturityDate, today.plusDays(30)); + case "within90" -> wrapper.gt(BillLedger::getMaturityDate, today.plusDays(30)) + .le(BillLedger::getMaturityDate, today.plusDays(90)); + case "over90" -> wrapper.gt(BillLedger::getMaturityDate, today.plusDays(90)); + default -> throw new ServiceException("到期快捷筛选不合法"); + } + } + + private BillLedger existing(Long id) { + BillLedger entity = getById(id); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) { + throw new ServiceException("汇票台账不存在"); + } + return entity; + } + + private BillLedger locked(Long id) { + BillLedger entity = baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedger::getId, id).last("FOR UPDATE")); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) { + throw new ServiceException("汇票台账不存在"); + } + return entity; + } + + private CustomerArchive customer(Long id, String name) { + if (id == null) throw new ServiceException(name + "不能为空"); + CustomerArchive customer = customerArchiveMapper.selectById(id); + if (customer == null || Objects.equals(customer.getIsDeleted(), 1)) { + throw new ServiceException(name + "对应的客商档案不存在"); + } + return customer; + } + + private BigDecimal activeUsedAmount(Long ledgerId) { + return usageMapper.selectList(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getBillLedgerId, ledgerId) + .eq(BillLedgerUsage::getUsageStatus, APPROVED)).stream() + .map(BillLedgerUsage::getUsedAmount) + .map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private boolean departmentAvailable(BillLedger ledger, Long deptId) { + List values = parseArray(ledger.getAvailableDeptIdsJson(), "可用部门"); + if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true; + return deptId == null || values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value))); + } + + private List parseArray(String value, String name) { + if (Func.isEmpty(value)) return List.of(); + try { + Object parsed = JsonUtil.parse(value, List.class); + return parsed instanceof List list ? list : List.of(); + } catch (Exception exception) { + throw new ServiceException(name + "格式不正确"); + } + } + + private String customerName(CustomerArchive customer) { + return Func.isNotEmpty(customer.getFullName()) ? customer.getFullName() : customer.getShortName(); + } + + private BigDecimal calculateDiscountFee(BigDecimal faceAmount, BigDecimal rate) { + if (rate == null) return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + return faceAmount.multiply(rate).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + } + + private BigDecimal positive(BigDecimal value, String name) { + if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException(name + "必须大于0"); + } + if (value.scale() > 2) throw new ServiceException(name + "最多保留2位小数"); + return value; + } + + private BigDecimal rate(BigDecimal value, String name) { + if (value == null) return null; + if (value.compareTo(BigDecimal.ZERO) < 0 || value.compareTo(BigDecimal.valueOf(100)) > 0) { + throw new ServiceException(name + "必须在0-100之间"); + } + return value.setScale(Math.min(value.scale(), 4), RoundingMode.HALF_UP); + } + + private BigDecimal money(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private String required(String value, String name, int length) { + if (Func.isEmpty(value) || value.trim().isEmpty()) throw new ServiceException(name + "不能为空"); + return limit(value.trim(), length, name); + } + + private String limit(String value, int length, String name) { + if (value != null && value.length() > length) { + throw new ServiceException(name + "不能超过" + length + "个字符"); + } + return value; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java new file mode 100644 index 0000000..15125ee --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java @@ -0,0 +1,343 @@ +/** + * 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.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.SysCache; +import org.springblade.transport.mapper.BillLedgerMapper; +import org.springblade.transport.mapper.BillLedgerUsageMapper; +import org.springblade.transport.mapper.BillPaymentMapper; +import org.springblade.transport.pojo.dto.BillPaymentSaveRequest; +import org.springblade.transport.pojo.dto.BillPaymentStatusRequest; +import org.springblade.transport.pojo.entity.BillLedger; +import org.springblade.transport.pojo.entity.BillLedgerUsage; +import org.springblade.transport.pojo.entity.BillPayment; +import org.springblade.transport.pojo.vo.BillPaymentVO; +import org.springblade.transport.service.IBillPaymentService; +import org.springblade.transport.wrapper.BillPaymentWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; + +/** 汇票付款服务实现。 @author Chill */ +@Service +@RequiredArgsConstructor +public class BillPaymentServiceImpl extends BaseServiceImpl + implements IBillPaymentService { + private static final String DRAFT = "draft"; + private static final String REVIEWING = "reviewing"; + private static final String APPROVED = "approved"; + private static final String RETURNED = "returned"; + private static final String VOIDED = "voided"; + private static final String RELEASED = "released"; + + private final BillLedgerMapper billLedgerMapper; + private final BillLedgerUsageMapper usageMapper; + + @Override + public IPage selectPage(IPage page, BillPaymentVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getPaymentNo()), BillPayment::getPaymentNo, query.getPaymentNo()) + .like(Func.isNotEmpty(query.getDeptName()), BillPayment::getDeptName, query.getDeptName()) + .ge(query.getPaymentStartDate() != null, BillPayment::getPaymentDate, query.getPaymentStartDate()) + .le(query.getPaymentEndDate() != null, BillPayment::getPaymentDate, query.getPaymentEndDate()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), BillPayment::getApprovalStatus, + query.getApprovalStatus()) + .orderByDesc(BillPayment::getCreateTime); + return page(page, wrapper).convert(item -> BillPaymentWrapper.build().entityVO(item)); + } + + @Override + public BillPaymentVO detail(Long id) { + BillPayment entity = existing(id); + BillPaymentVO vo = BillPaymentWrapper.build().entityVO(entity); + BillLedger ledger = billLedgerMapper.selectById(entity.getBillLedgerId()); + if (ledger != null) { + vo.setBillNo(ledger.getBillNo()); + vo.setFaceAmount(ledger.getFaceAmount()); + vo.setAvailableBalance(ledger.getAvailableBalance()); + } + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(BillPaymentSaveRequest request) { + validateRequest(request); + BillPayment entity = request.getId() == null ? new BillPayment() : locked(request.getId()); + if (entity.getId() != null && !List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) { + throw new ServiceException("当前状态不可编辑"); + } + Long deptId = Func.firstLong(AuthUtil.getDeptId()); + if (deptId == null) { + throw new ServiceException("使用部门不能为空"); + } + if (entity.getId() != null && !Objects.equals(entity.getDeptId(), deptId)) { + throw new ServiceException("仅允许编辑当前部门的汇票付款"); + } + String deptName = required(SysCache.getDeptName(deptId), "使用部门", 100); + BillLedger ledger = lockedBill(request.getBillLedgerId()); + BigDecimal usedAmount = positive(request.getUsedAmount(), "本次使用"); + validateLedgerAmount(ledger, usedAmount, deptId); + if (entity.getId() == null) { + entity.setPaymentNo(nextNo()); + entity.setApprovalStatus(DRAFT); + entity.setCurrentNode("草稿"); + } + entity.setBillLedgerId(ledger.getId()); + entity.setBillNo(ledger.getBillNo()); + entity.setFaceAmount(money(ledger.getFaceAmount()).setScale(2, RoundingMode.HALF_UP)); + entity.setAvailableBalance(money(ledger.getAvailableBalance()).setScale(2, RoundingMode.HALF_UP)); + entity.setUsedAmount(usedAmount); + entity.setDeptId(deptId); + entity.setDeptName(deptName); + entity.setPaymentDate(request.getPaymentDate() == null ? LocalDate.now() : request.getPaymentDate()); + entity.setAttachmentsJson(request.getAttachmentsJson()); + entity.setRemark(limit(request.getRemark(), 200, "备注")); + entity.setStatus(1); + saveOrUpdate(entity); + return entity.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + BillPayment entity = locked(id); + if (!DRAFT.equals(entity.getApprovalStatus())) { + throw new ServiceException("仅草稿状态的汇票付款允许删除"); + } + removeById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(BillPaymentStatusRequest request) { + BillPayment entity = locked(requiredId(request)); + if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) { + throw new ServiceException("当前状态不允许提交"); + } + validateStoredAmount(entity); + entity.setApprovalStatus(REVIEWING); + entity.setCurrentNode("财务审核"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(BillPaymentStatusRequest request) { + BillPayment entity = locked(requiredId(request)); + if (!REVIEWING.equals(entity.getApprovalStatus())) { + throw new ServiceException("仅审批中的汇票付款允许审核"); + } + BillLedger ledger = lockedBill(entity.getBillLedgerId()); + validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId()); + BillLedgerUsage exists = usageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getBillPaymentId, entity.getId()) + .eq(BillLedgerUsage::getUsageStatus, APPROVED) + .last("FOR UPDATE")); + if (exists != null) { + throw new ServiceException("该汇票付款已生成使用记录"); + } + BigDecimal usedAmount = money(entity.getUsedAmount()).setScale(2, RoundingMode.HALF_UP); + ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(usedAmount) + .setScale(2, RoundingMode.HALF_UP)); + billLedgerMapper.updateById(ledger); + BillLedgerUsage usage = new BillLedgerUsage(); + usage.setBillLedgerId(ledger.getId()); + usage.setBillPaymentId(entity.getId()); + usage.setApplicationNo(entity.getPaymentNo()); + usage.setUsedAmount(usedAmount); + usage.setUseDeptId(entity.getDeptId()); + usage.setUseDeptName(entity.getDeptName()); + usage.setUsageStatus(APPROVED); + usage.setStatus(1); + usageMapper.insert(usage); + entity.setAvailableBalance(ledger.getAvailableBalance()); + entity.setApprovalStatus(APPROVED); + entity.setCurrentNode("审批通过"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void returnBill(BillPaymentStatusRequest request) { + BillPayment entity = locked(requiredId(request)); + if (!REVIEWING.equals(entity.getApprovalStatus())) { + throw new ServiceException("仅审批中的汇票付款允许驳回"); + } + entity.setApprovalStatus(RETURNED); + entity.setCurrentNode("已驳回"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200, "驳回原因")); + updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void voidBill(BillPaymentStatusRequest request) { + BillPayment entity = locked(requiredId(request)); + if (!APPROVED.equals(entity.getApprovalStatus())) { + throw new ServiceException("仅审批通过的汇票付款允许作废"); + } + BillLedgerUsage usage = usageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getBillPaymentId, entity.getId()) + .eq(BillLedgerUsage::getUsageStatus, APPROVED) + .last("FOR UPDATE")); + if (usage == null) { + throw new ServiceException("未找到汇票使用记录,无法作废"); + } + BillLedger ledger = lockedBill(usage.getBillLedgerId()); + ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount())) + .min(money(ledger.getFaceAmount())).setScale(2, RoundingMode.HALF_UP)); + billLedgerMapper.updateById(ledger); + usage.setUsageStatus(RELEASED); + usageMapper.updateById(usage); + entity.setAvailableBalance(ledger.getAvailableBalance()); + entity.setApprovalStatus(VOIDED); + entity.setCurrentNode("已作废"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200, "作废原因")); + updateById(entity); + } + + private void validateRequest(BillPaymentSaveRequest request) { + if (request == null) throw new ServiceException("请求参数不能为空"); + if (request.getBillLedgerId() == null) throw new ServiceException("票据号码不能为空"); + if (request.getPaymentDate() == null) throw new ServiceException("付款日期不能为空"); + positive(request.getUsedAmount(), "本次使用"); + } + + private void validateStoredAmount(BillPayment entity) { + BillLedger ledger = lockedBill(entity.getBillLedgerId()); + validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId()); + entity.setBillNo(ledger.getBillNo()); + entity.setFaceAmount(ledger.getFaceAmount()); + entity.setAvailableBalance(ledger.getAvailableBalance()); + } + + private void validateLedgerAmount(BillLedger ledger, BigDecimal usedAmount, Long deptId) { + if (ledger.getMaturityDate() != null && ledger.getMaturityDate().isBefore(LocalDate.now())) { + throw new ServiceException("所选汇票已到期"); + } + if (usedAmount == null || usedAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("本次使用必须大于0"); + } + if (usedAmount.scale() > 2) { + throw new ServiceException("本次使用最多保留2位小数"); + } + if (usedAmount.compareTo(money(ledger.getAvailableBalance())) > 0) { + throw new ServiceException("本次使用不能超过汇票可用余额"); + } + if (!departmentAvailable(ledger, deptId)) { + throw new ServiceException("当前使用部门不在汇票可用部门范围内"); + } + } + + private boolean departmentAvailable(BillLedger ledger, Long deptId) { + if (Func.isEmpty(ledger.getAvailableDeptIdsJson())) return false; + try { + Object parsed = JsonUtil.parse(ledger.getAvailableDeptIdsJson(), List.class); + if (!(parsed instanceof List values)) return false; + if (values.stream().anyMatch(item -> "all".equals(String.valueOf(item)))) return true; + return deptId != null && values.stream().anyMatch(item -> String.valueOf(deptId).equals(String.valueOf(item))); + } catch (Exception exception) { + throw new ServiceException("汇票可用部门配置不正确"); + } + } + + private BillLedger lockedBill(Long id) { + BillLedger ledger = billLedgerMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedger::getId, id).last("FOR UPDATE")); + if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) { + throw new ServiceException("所选汇票台账不存在"); + } + return ledger; + } + + private BillPayment locked(Long id) { + BillPayment entity = baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillPayment::getId, id).last("FOR UPDATE")); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) { + throw new ServiceException("汇票付款不存在"); + } + return entity; + } + + private BillPayment existing(Long id) { + BillPayment entity = getById(id); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) { + throw new ServiceException("汇票付款不存在"); + } + return entity; + } + + private Long requiredId(BillPaymentStatusRequest request) { + if (request == null || request.getId() == null) throw new ServiceException("单据不能为空"); + return request.getId(); + } + + private synchronized String nextNo() { + String prefix = "HP" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + return prefix + String.format("%05d", count(Wrappers.lambdaQuery() + .likeRight(BillPayment::getPaymentNo, prefix)) + 1); + } + + private BigDecimal positive(BigDecimal value, String name) { + if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException(name + "必须大于0"); + if (value.scale() > 2) throw new ServiceException(name + "最多保留2位小数"); + return value.setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal money(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private String required(String value, String name, int length) { + if (Func.isEmpty(value) || value.trim().isEmpty()) throw new ServiceException(name + "不能为空"); + return limit(value.trim(), length, name); + } + + private String limit(String value, int length, String name) { + if (value != null && value.length() > length) throw new ServiceException(name + "不能超过" + length + "个字符"); + return value; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java new file mode 100644 index 0000000..63cb624 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -0,0 +1,643 @@ +/** + * 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.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.CustomerArchiveMapper; +import org.springblade.transport.mapper.CustomerContactMapper; +import org.springblade.transport.mapper.CustomerInvoiceInfoMapper; +import org.springblade.transport.mapper.FormalSettlementDetailMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.InvoiceApplicationDetailMapper; +import org.springblade.transport.mapper.InvoiceApplicationLineMapper; +import org.springblade.transport.mapper.InvoiceApplicationMapper; +import org.springblade.transport.mapper.InvoiceApplicationRecordMapper; +import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper; +import org.springblade.transport.mapper.InvoiceApplicationSheetMapper; +import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest; +import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest; +import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.CustomerContact; +import org.springblade.transport.pojo.entity.CustomerInvoiceInfo; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.InvoiceApplication; +import org.springblade.transport.pojo.entity.InvoiceApplicationDetail; +import org.springblade.transport.pojo.entity.InvoiceApplicationLine; +import org.springblade.transport.pojo.entity.InvoiceApplicationRecord; +import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement; +import org.springblade.transport.pojo.entity.InvoiceApplicationSheet; +import org.springblade.transport.pojo.vo.InvoiceApplicationSheetVO; +import org.springblade.transport.pojo.vo.InvoiceApplicationVO; +import org.springblade.transport.service.IInvoiceApplicationService; +import org.springblade.transport.wrapper.InvoiceApplicationWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 开票申请服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class InvoiceApplicationServiceImpl extends BaseServiceImpl + implements IInvoiceApplicationService { + private static final String DRAFT = "draft"; + private static final String REVIEWING = "reviewing"; + private static final String APPROVED = "approved"; + private static final String RETURNED = "returned"; + private static final String VOIDED = "voided"; + private final InvoiceApplicationSettlementMapper settlementRelationMapper; + private final InvoiceApplicationSheetMapper sheetMapper; + private final InvoiceApplicationLineMapper lineMapper; + private final InvoiceApplicationDetailMapper applicationDetailMapper; + private final InvoiceApplicationRecordMapper recordMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final FormalSettlementDetailMapper formalSettlementDetailMapper; + private final CustomerArchiveMapper customerArchiveMapper; + private final CustomerInvoiceInfoMapper customerInvoiceInfoMapper; + private final CustomerContactMapper customerContactMapper; + + @Override + public IPage selectPage(IPage page, InvoiceApplicationVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getApplicationNo()), InvoiceApplication::getApplicationNo, query.getApplicationNo()) + .like(Func.isNotEmpty(query.getProjectName()), InvoiceApplication::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), InvoiceApplication::getDeptName, query.getDeptName()) + .eq(Func.isNotEmpty(query.getKingdeeStatus()), InvoiceApplication::getKingdeeStatus, query.getKingdeeStatus()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), InvoiceApplication::getApprovalStatus, query.getApprovalStatus()) + .orderByDesc(InvoiceApplication::getCreateTime); + return page(page, wrapper).convert(this::toListVO); + } + + @Override + public InvoiceApplicationVO detail(Long id) { + InvoiceApplication entity = existing(id); + InvoiceApplicationVO vo = toListVO(entity); + vo.setSettlements(settlementRelationMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id) + .orderByAsc(InvoiceApplicationSettlement::getCreateTime))); + List sheets = sheetMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo)); + vo.setSheets(sheets.stream().map(sheet -> { + InvoiceApplicationSheetVO sheetVO = Objects.requireNonNull(BeanUtil.copyProperties(sheet, InvoiceApplicationSheetVO.class)); + sheetVO.setLines(lineMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationLine::getInvoiceSheetId, sheet.getId()).orderByAsc(InvoiceApplicationLine::getLineNo))); + return sheetVO; + }).toList()); + vo.setDetails(applicationDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationDetail::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationDetail::getLineNo))); + vo.setRecords(recordMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationRecord::getInvoiceApplicationId, id) + .orderByAsc(InvoiceApplicationRecord::getCreateTime))); + return vo; + } + + @Override + public List> settlementCandidates(String keyword) { + List settlements = formalSettlementMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlement::getSettlementType, "receivable") + .eq(FormalSettlement::getApprovalStatus, APPROVED) + .eq(FormalSettlement::getStatus, 1) + .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(FormalSettlement::getFormalSettlementNo, keyword) + .or().like(FormalSettlement::getProjectName, keyword) + .or().like(FormalSettlement::getContractName, keyword)) + .orderByDesc(FormalSettlement::getCreateTime).last("limit 200")); + return settlements.stream().map(settlement -> { + BigDecimal available = availableAmount(settlement, null); + Map row = new LinkedHashMap<>(); + row.put("id", settlement.getId()); + row.put("formalSettlementNo", settlement.getFormalSettlementNo()); + row.put("projectId", settlement.getProjectId()); + row.put("projectName", settlement.getProjectName()); + row.put("deptId", settlement.getDeptId()); + row.put("deptName", settlement.getDeptName()); + row.put("contractId", settlement.getContractId()); + row.put("contractNo", settlement.getContractNo()); + row.put("contractName", settlement.getContractName()); + row.put("issuerName", settlement.getPayeeName()); + row.put("receiverName", settlement.getPayerName()); + row.put("settlementAmount", money(settlement.getSettlementAmount())); + row.put("availableInvoiceAmount", available); + return row; + }).filter(row -> ((BigDecimal) row.get("availableInvoiceAmount")).compareTo(BigDecimal.ZERO) > 0).toList(); + } + + @Override + public List settlementDetails(String settlementIds) { + List ids = distinctIds(settlementIds); + if (ids.isEmpty()) return List.of(); + assertCompatible(ids.stream().map(this::availableSettlement).toList()); + return formalSettlementDetailMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlementDetail::getFormalSettlementId, ids).orderByAsc(FormalSettlementDetail::getLineNo)); + } + + @Override + public Map receiverInformation(String settlementIds) { + List settlements = distinctIds(settlementIds).stream().map(this::availableSettlement).toList(); + assertCompatible(settlements); + FormalSettlement first = settlements.get(0); + CustomerArchive customer = findCustomer(first.getPayerName()); + Map result = new LinkedHashMap<>(); + result.put("issuerName", first.getPayeeName()); + result.put("receiverName", first.getPayerName()); + result.put("customer", customer); + result.put("invoiceInfos", customer == null ? List.of() : customerInvoiceInfoMapper.selectList( + Wrappers.lambdaQuery().eq(CustomerInvoiceInfo::getCustomerId, customer.getId()) + .eq(CustomerInvoiceInfo::getStatus, 1).orderByDesc(CustomerInvoiceInfo::getIsDefault))); + result.put("contacts", customer == null ? List.of() : customerContactMapper.selectList( + Wrappers.lambdaQuery().eq(CustomerContact::getCustomerId, customer.getId()) + .eq(CustomerContact::getStatus, 1).orderByDesc(CustomerContact::getIsDefault))); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(InvoiceApplicationSaveRequest request) { + validateRequest(request); + boolean creating = request.getId() == null; + InvoiceApplication entity = creating ? new InvoiceApplication() : editable(request.getId()); + List oldSettlementIds = entity.getId() == null ? List.of() : relationSettlementIds(entity.getId()); + List requestedRows = request.getSettlements().stream() + .filter(Objects::nonNull) + .peek(row -> { + if (row.getSettlementId() == null) throw new ServiceException("正式结算单不能为空"); + }) + .collect(Collectors.toMap(InvoiceApplicationSaveRequest.SettlementRow::getSettlementId, + Function.identity(), (first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList(); + List settlements = requestedRows.stream().map(row -> availableSettlement(row.getSettlementId())).toList(); + assertCompatible(settlements); + FormalSettlement first = settlements.get(0); + Map settlementMap = settlements.stream() + .collect(Collectors.toMap(FormalSettlement::getId, Function.identity())); + BigDecimal totalAvailable = BigDecimal.ZERO; + BigDecimal allocatedTotal = BigDecimal.ZERO; + for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) { + FormalSettlement settlement = settlementMap.get(row.getSettlementId()); + BigDecimal available = availableAmount(settlement, entity.getId()); + BigDecimal allocated = nonNegative(row.getAllocatedInvoiceAmount(), "分摊发票金额"); + if (allocated.compareTo(available) > 0) { + throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + "的分摊金额超过剩余可开票金额"); + } + totalAvailable = totalAvailable.add(available); + allocatedTotal = allocatedTotal.add(allocated); + } + BigDecimal lineTotal = validateSheets(request.getSheets()); + if (lineTotal.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("本次开票金额必须大于0"); + } + if (lineTotal.compareTo(allocatedTotal) != 0) { + throw new ServiceException("开票商品行金额合计必须等于结算单分摊金额合计"); + } + CustomerArchive customer = findCustomer(first.getPayerName()); + CustomerInvoiceInfo invoiceInfo = customerInvoiceInfoMapper.selectById(request.getReceiverInvoiceInfoId()); + if (customer == null || invoiceInfo == null || !Objects.equals(customer.getId(), invoiceInfo.getCustomerId()) + || Objects.equals(invoiceInfo.getIsDeleted(), 1) || !Objects.equals(invoiceInfo.getStatus(), 1)) { + throw new ServiceException("请选择受票方有效的开票信息"); + } + String invoiceTitle = required(invoiceInfo.getInvoiceTitle(), "受票方单位"); + String taxpayerNo = limit(invoiceInfo.getTaxNo(), 20, "纳税人识别号"); + String bankName = limit(invoiceInfo.getBankName(), 100, "开户行"); + String bankAccount = limit(invoiceInfo.getBankAccount(), 50, "开户账号"); + String registeredAddress = limit(invoiceInfo.getRegisteredAddress(), 200, "注册地址"); + if ("electronic_special".equals(request.getInvoiceType())) { + required(taxpayerNo, "电子专票的纳税人识别号"); + required(bankName, "电子专票的开户行"); + required(bankAccount, "电子专票的开户账号"); + required(registeredAddress, "电子专票的注册地址"); + } + if (entity.getId() == null) { + entity.setApplicationNo(nextNo()); + entity.setApprovalStatus(DRAFT); + entity.setCurrentNode("草稿"); + entity.setKingdeeStatus("unsynced"); + entity.setApplicantName(AuthUtil.getUserName()); + } + entity.setProjectId(first.getProjectId()); + entity.setProjectName(first.getProjectName()); + entity.setDeptId(first.getDeptId()); + entity.setDeptName(first.getDeptName()); + entity.setIssuerName(first.getPayeeName()); + entity.setReceiverCustomerId(customer.getId()); + entity.setReceiverName(invoiceTitle); + entity.setInvoiceType(request.getInvoiceType()); + entity.setAvailableInvoiceAmount(totalAvailable); + entity.setInvoiceAmount(lineTotal); + entity.setUndertakingDeptId(first.getDeptId()); + entity.setUndertakingDeptName(first.getDeptName()); + entity.setDepartmentEmails(normalizeEmails(request.getDepartmentEmails())); + entity.setReceiverInvoiceInfoId(invoiceInfo.getId()); + entity.setTaxpayerNo(taxpayerNo); + entity.setBankName(bankName); + entity.setBankAccount(bankAccount); + entity.setRegisteredAddress(registeredAddress); + entity.setContactName(limit(request.getContactName(), 50, "联系人")); + entity.setContactPhone(validatePhone(request.getContactPhone())); + entity.setEmail(validateReceiverEmails(request.getEmail())); + entity.setAttachmentsJson(request.getAttachmentsJson()); + entity.setRemark(limit(request.getRemark(), 200, "备注")); + saveOrUpdate(entity); + deleteChildren(entity.getId()); + saveRelations(entity.getId(), requestedRows, settlementMap); + saveSheets(entity.getId(), request.getSheets()); + saveDetails(entity.getId(), request.getDetailIds(), settlementMap.keySet()); + Set refreshIds = new LinkedHashSet<>(oldSettlementIds); + refreshIds.addAll(settlementMap.keySet()); + refreshIds.forEach(this::refreshSettlementInvoiceStatus); + record(entity.getId(), creating ? "create" : "save", creating ? "创建草稿" : "保存草稿", + entity.getApprovalStatus(), entity.getApprovalStatus(), null, null); + return entity.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + InvoiceApplication entity = editable(id); + List settlementIds = relationSettlementIds(id); + deleteChildren(id); + removeById(entity); + settlementIds.forEach(this::refreshSettlementInvoiceStatus); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(InvoiceApplicationStatusRequest request) { + InvoiceApplication entity = editable(request.getId()); + String fromStatus = entity.getApprovalStatus(); + entity.setApprovalStatus(REVIEWING); + entity.setCurrentNode("开票审核"); + entity.setCurrentProcessor(null); + entity.setApplicationDate(LocalDate.now()); + updateById(entity); + record(entity.getId(), "submit", "提交审批", fromStatus, REVIEWING, null, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(InvoiceApplicationStatusRequest request) { + changeStatus(request.getId(), REVIEWING, APPROVED, "approve", "审批通过", null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void returnBill(InvoiceApplicationStatusRequest request) { + changeStatus(request.getId(), REVIEWING, RETURNED, "return", "已驳回", + required(request.getReason(), "驳回原因")); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void voidBill(InvoiceApplicationStatusRequest request) { + InvoiceApplication entity = existing(request.getId()); + if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的开票申请允许作废"); + entity.setApprovalStatus(VOIDED); + entity.setCurrentNode("已作废"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + entity.setVoidReason(limit(required(request.getReason(), "作废原因"), 200, "作废原因")); + updateById(entity); + record(entity.getId(), "void", "作废", APPROVED, VOIDED, entity.getVoidReason(), entity.getKingdeeBillNo()); + relationSettlementIds(entity.getId()).forEach(this::refreshSettlementInvoiceStatus); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String syncKingdee(Long id) { + InvoiceApplication entity = existing(id); + if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的开票申请允许同步金蝶"); + if ("synced".equals(entity.getKingdeeStatus())) return entity.getKingdeeBillNo(); + String kingdeeNo = "K3INV" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + entity.setKingdeeBillNo(kingdeeNo); + entity.setKingdeeStatus("synced"); + entity.setSyncedTime(LocalDateTime.now()); + updateById(entity); + record(entity.getId(), "sync", "同步金蝶", entity.getApprovalStatus(), entity.getApprovalStatus(), null, kingdeeNo); + return kingdeeNo; + } + + private InvoiceApplicationVO toListVO(InvoiceApplication entity) { + InvoiceApplicationVO vo = InvoiceApplicationWrapper.build().entityVO(entity); + vo.setSettlementNos(settlementRelationMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSettlement::getInvoiceApplicationId, entity.getId()) + .orderByAsc(InvoiceApplicationSettlement::getCreateTime)).stream() + .map(InvoiceApplicationSettlement::getFormalSettlementNo).collect(Collectors.joining(","))); + return vo; + } + + private void validateRequest(InvoiceApplicationSaveRequest request) { + if (request.getSettlements() == null || request.getSettlements().isEmpty()) throw new ServiceException("请至少选择一张正式结算单"); + if (request.getDetailIds() == null || request.getDetailIds().isEmpty()) throw new ServiceException("请至少选择一条开票明细"); + if (!List.of("electronic_special", "electronic_normal").contains(request.getInvoiceType())) throw new ServiceException("请选择有效的发票类型"); + if (request.getReceiverInvoiceInfoId() == null) throw new ServiceException("请选择受票方单位"); + if (Func.isEmpty(request.getDepartmentEmails())) throw new ServiceException("请选择部门邮箱"); + } + + private BigDecimal validateSheets(List sheets) { + if (sheets == null || sheets.isEmpty()) throw new ServiceException("请至少添加一张发票"); + BigDecimal total = BigDecimal.ZERO; + for (InvoiceApplicationSaveRequest.SheetRow sheet : sheets) { + if (sheet.getLines() == null || sheet.getLines().isEmpty()) throw new ServiceException("每张发票至少需要一条商品行"); + boolean containsFreight = sheet.getLines().stream().anyMatch(line -> "运费".equals(line.getGoodsName())); + boolean containsOther = sheet.getLines().stream().anyMatch(line -> !"运费".equals(line.getGoodsName())); + if (containsFreight && containsOther) throw new ServiceException("运费不能与其他费用合并开在同一张发票中"); + for (InvoiceApplicationSaveRequest.LineRow line : sheet.getLines()) { + required(line.getGoodsCategory(), "商品和服务分类"); + required(line.getGoodsName(), "货物或服务简称"); + nonNegative(line.getQuantity(), "数量"); + nonNegative(line.getUnitPriceNoTax(), "不含税单价"); + BigDecimal amount = nonNegative(line.getAmountWithTax(), "含税金额"); + BigDecimal taxRate = nonNegative(line.getTaxRate(), "税率"); + if (taxRate.compareTo(BigDecimal.valueOf(100)) > 0) throw new ServiceException("税率必须在0-100之间"); + line.setTaxAmount(calculateTax(amount, taxRate)); + line.setRemark(limit(line.getRemark(), 200, "商品行备注")); + total = total.add(amount); + } + } + return total.setScale(2, RoundingMode.HALF_UP); + } + + private void assertCompatible(List settlements) { + if (settlements.isEmpty()) throw new ServiceException("请选择正式结算单"); + FormalSettlement first = settlements.get(0); + if (settlements.stream().anyMatch(item -> !Objects.equals(first.getContractId(), item.getContractId()) + || !Objects.equals(first.getProjectId(), item.getProjectId()) + || !Objects.equals(first.getDeptId(), item.getDeptId()) + || !Objects.equals(first.getPayerName(), item.getPayerName()) + || !Objects.equals(first.getPayeeName(), item.getPayeeName()))) { + throw new ServiceException("合并开票的正式结算单必须属于同一合同、项目、组织及收付款方"); + } + } + + private FormalSettlement availableSettlement(Long id) { + if (id == null) throw new ServiceException("正式结算单不能为空"); + FormalSettlement settlement = formalSettlementMapper.selectById(id); + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) throw new ServiceException("正式结算单不存在"); + if (!Objects.equals(settlement.getStatus(), 1) || !APPROVED.equals(settlement.getApprovalStatus()) + || !"receivable".equals(settlement.getSettlementType())) { + throw new ServiceException("只能选择审批通过、未作废的应收正式结算单"); + } + return settlement; + } + + private BigDecimal availableAmount(FormalSettlement settlement, Long excludeApplicationId) { + BigDecimal allocated = activeRelations(settlement.getId(), excludeApplicationId).stream() + .map(InvoiceApplicationSettlement::getAllocatedInvoiceAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + return money(settlement.getSettlementAmount()).subtract(allocated).max(BigDecimal.ZERO); + } + + private List activeRelations(Long settlementId, Long excludeApplicationId) { + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(InvoiceApplicationSettlement::getFormalSettlementId, settlementId) + .ne(excludeApplicationId != null, InvoiceApplicationSettlement::getInvoiceApplicationId, excludeApplicationId)); + if (relations.isEmpty()) return List.of(); + Map applications = listByIds(relations.stream() + .map(InvoiceApplicationSettlement::getInvoiceApplicationId).distinct().toList()).stream() + .collect(Collectors.toMap(InvoiceApplication::getId, Function.identity())); + return relations.stream().filter(relation -> { + InvoiceApplication application = applications.get(relation.getInvoiceApplicationId()); + return application != null && !VOIDED.equals(application.getApprovalStatus()) + && !Objects.equals(application.getIsDeleted(), 1); + }).toList(); + } + + private void saveRelations(Long applicationId, List rows, + Map settlementMap) { + for (InvoiceApplicationSaveRequest.SettlementRow row : rows) { + FormalSettlement source = settlementMap.get(row.getSettlementId()); + InvoiceApplicationSettlement relation = new InvoiceApplicationSettlement(); + relation.setInvoiceApplicationId(applicationId); + relation.setFormalSettlementId(source.getId()); + relation.setFormalSettlementNo(source.getFormalSettlementNo()); + relation.setSettlementAmount(source.getSettlementAmount()); + relation.setAvailableInvoiceAmount(availableAmount(source, applicationId)); + relation.setAllocatedInvoiceAmount(row.getAllocatedInvoiceAmount()); + settlementRelationMapper.insert(relation); + } + } + + private void saveSheets(Long applicationId, List sheets) { + int sheetNo = 1; + for (InvoiceApplicationSaveRequest.SheetRow sheetRow : sheets) { + InvoiceApplicationSheet sheet = new InvoiceApplicationSheet(); + sheet.setInvoiceApplicationId(applicationId); + sheet.setSheetNo(sheetNo++); + sheet.setInvoiceAmount(sheetRow.getLines().stream().map(InvoiceApplicationSaveRequest.LineRow::getAmountWithTax) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + sheetMapper.insert(sheet); + int lineNo = 1; + for (InvoiceApplicationSaveRequest.LineRow lineRow : sheetRow.getLines()) { + InvoiceApplicationLine line = Objects.requireNonNull(BeanUtil.copyProperties(lineRow, InvoiceApplicationLine.class)); + line.setId(null); + line.setInvoiceApplicationId(applicationId); + line.setInvoiceSheetId(sheet.getId()); + line.setLineNo(lineNo++); + lineMapper.insert(line); + } + } + } + + private void saveDetails(Long applicationId, List detailIds, Set settlementIds) { + List details = formalSettlementDetailMapper.selectBatchIds(detailIds.stream().distinct().toList()); + if (details.size() != detailIds.stream().distinct().count() + || details.stream().anyMatch(detail -> !settlementIds.contains(detail.getFormalSettlementId()))) { + throw new ServiceException("开票明细必须来自已选择的正式结算单"); + } + int lineNo = 1; + for (FormalSettlementDetail source : details) { + InvoiceApplicationDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(source, InvoiceApplicationDetail.class)); + detail.setId(null); + detail.setInvoiceApplicationId(applicationId); + detail.setFormalSettlementDetailId(source.getId()); + detail.setLineNo(lineNo++); + applicationDetailMapper.insert(detail); + } + } + + private void deleteChildren(Long applicationId) { + List sheetIds = sheetMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSheet::getInvoiceApplicationId, applicationId)).stream() + .map(InvoiceApplicationSheet::getId).toList(); + if (!sheetIds.isEmpty()) lineMapper.delete(Wrappers.lambdaQuery() + .in(InvoiceApplicationLine::getInvoiceSheetId, sheetIds)); + sheetMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSheet::getInvoiceApplicationId, applicationId)); + applicationDetailMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceApplicationDetail::getInvoiceApplicationId, applicationId)); + settlementRelationMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSettlement::getInvoiceApplicationId, applicationId)); + } + + private void refreshSettlementInvoiceStatus(Long settlementId) { + FormalSettlement settlement = formalSettlementMapper.selectById(settlementId); + if (settlement == null) return; + BigDecimal allocated = activeRelations(settlementId, null).stream() + .map(InvoiceApplicationSettlement::getAllocatedInvoiceAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + String status = allocated.compareTo(BigDecimal.ZERO) <= 0 ? "unreceived" + : allocated.compareTo(money(settlement.getSettlementAmount())) >= 0 ? "completed" : "partial"; + settlement.setInvoiceStatus(status); + formalSettlementMapper.updateById(settlement); + } + + private List relationSettlementIds(Long applicationId) { + return settlementRelationMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSettlement::getInvoiceApplicationId, applicationId)).stream() + .map(InvoiceApplicationSettlement::getFormalSettlementId).distinct().toList(); + } + + private CustomerArchive findCustomer(String name) { + if (Func.isEmpty(name)) return null; + return customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, name).or().eq(CustomerArchive::getShortName, name)) + .eq(CustomerArchive::getStatus, 1) + .eq(CustomerArchive::getIsDeleted, 0).last("limit 1")); + } + + private void changeStatus(Long id, String from, String to, String actionType, String node, String reason) { + InvoiceApplication entity = existing(id); + if (!from.equals(entity.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作"); + entity.setApprovalStatus(to); + entity.setCurrentNode(node); + entity.setCurrentProcessor(AuthUtil.getUserName()); + updateById(entity); + record(id, actionType, node, from, to, reason, entity.getKingdeeBillNo()); + } + + private void record(Long applicationId, String actionType, String actionName, String fromStatus, + String toStatus, String reason, String kingdeeBillNo) { + InvoiceApplicationRecord record = new InvoiceApplicationRecord(); + record.setInvoiceApplicationId(applicationId); + record.setActionType(actionType); + record.setActionName(actionName); + record.setFromStatus(fromStatus); + record.setToStatus(toStatus); + record.setOperatorName(AuthUtil.getUserName()); + record.setReason(reason); + record.setKingdeeBillNo(kingdeeBillNo); + recordMapper.insert(record); + } + + private InvoiceApplication existing(Long id) { + InvoiceApplication entity = getById(id); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("开票申请不存在"); + return entity; + } + + private InvoiceApplication editable(Long id) { + InvoiceApplication entity = existing(id); + if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) throw new ServiceException("当前状态不可编辑"); + return entity; + } + + private List distinctIds(String ids) { + return ids == null ? List.of() : Func.toLongList(ids).stream().distinct().toList(); + } + + private String nextNo() { + String prefix = "KP-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + return prefix + String.format("%05d", count(Wrappers.lambdaQuery() + .likeRight(InvoiceApplication::getApplicationNo, prefix)) + 1); + } + + private String normalizeEmails(String value) { + List emails = value == null ? List.of() : List.of(value.split("[;,,;]")); + List normalized = emails.stream().map(String::trim).filter(item -> !item.isEmpty()).distinct().toList(); + if (normalized.isEmpty() || normalized.size() > 3) throw new ServiceException("部门邮箱必填且最多选择3个"); + normalized.forEach(email -> validateEmail(email, "部门邮箱")); + return String.join(";", normalized); + } + + private String validateEmail(String value, String name) { + String result = limit(value, 100, name); + if (Func.isNotEmpty(result) && !result.matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) { + throw new ServiceException(name + "格式不正确"); + } + return result; + } + + private String validateReceiverEmails(String value) { + if (Func.isEmpty(value)) return value; + List emails = List.of(value.split("[;,,;]")).stream() + .map(String::trim).filter(item -> !item.isEmpty()).distinct().toList(); + if (emails.size() > 3) throw new ServiceException("邮箱最多填写3个"); + emails.forEach(email -> validateEmail(email, "邮箱")); + return limit(String.join(";", emails), 100, "邮箱"); + } + + private String validatePhone(String value) { + if (Func.isNotEmpty(value) && !value.matches("^\\d{11}$")) throw new ServiceException("联系电话必须为11位数字"); + return value; + } + + private BigDecimal calculateTax(BigDecimal amount, BigDecimal rate) { + if (rate.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + return amount.subtract(amount.divide(BigDecimal.ONE.add(rate.divide(BigDecimal.valueOf(100), 8, + RoundingMode.HALF_UP)), 8, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal nonNegative(BigDecimal value, String name) { + if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(name + "不能小于0"); + return value; + } + + private BigDecimal money(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private String required(String value, String name) { + if (Func.isEmpty(value)) throw new ServiceException(name + "不能为空"); + return value; + } + + private String limit(String value, int length, String name) { + if (value != null && value.length() > length) throw new ServiceException(name + "不能超过" + length + "个字符"); + return value; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java new file mode 100644 index 0000000..2d7e310 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java @@ -0,0 +1,684 @@ +/** + * 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.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.CustomerArchiveMapper; +import org.springblade.transport.mapper.CustomerContactMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.InvoiceReceiptMapper; +import org.springblade.transport.mapper.InvoiceReceiptRecordMapper; +import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper; +import org.springblade.transport.mapper.KingdeeInvoicePoolMapper; +import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest; +import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest; +import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.CustomerContact; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.InvoiceReceipt; +import org.springblade.transport.pojo.entity.InvoiceReceiptRecord; +import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement; +import org.springblade.transport.pojo.entity.KingdeeInvoicePool; +import org.springblade.transport.pojo.vo.InvoiceReceiptVO; +import org.springblade.transport.service.IInvoiceReceiptService; +import org.springblade.transport.wrapper.InvoiceReceiptWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 收票登记服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class InvoiceReceiptServiceImpl extends BaseServiceImpl + implements IInvoiceReceiptService { + + private static final String DRAFT = "draft"; + private static final String REVIEWING = "reviewing"; + private static final String APPROVED = "approved"; + private static final String RETURNED = "returned"; + private static final String VOIDED = "voided"; + + private final KingdeeInvoicePoolMapper invoicePoolMapper; + private final InvoiceReceiptSettlementMapper settlementRelationMapper; + private final InvoiceReceiptRecordMapper recordMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final CustomerArchiveMapper customerArchiveMapper; + private final CustomerContactMapper customerContactMapper; + + @Override + public IPage selectPage(IPage page, InvoiceReceiptVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getInvoiceNo()), InvoiceReceipt::getInvoiceNo, query.getInvoiceNo()) + .eq(query.getInvoiceDate() != null, InvoiceReceipt::getInvoiceDate, query.getInvoiceDate()) + .like(Func.isNotEmpty(query.getProjectName()), InvoiceReceipt::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), InvoiceReceipt::getDeptName, query.getDeptName()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), InvoiceReceipt::getApprovalStatus, + query.getApprovalStatus()) + .eq(Func.isNotEmpty(query.getKingdeeStatus()), InvoiceReceipt::getKingdeeStatus, + query.getKingdeeStatus()) + .orderByDesc(InvoiceReceipt::getCreateTime); + return page(page, wrapper).convert(this::toListVO); + } + + @Override + public InvoiceReceiptVO detail(Long id) { + InvoiceReceipt entity = existing(id); + InvoiceReceiptVO vo = toListVO(entity); + List settlements = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, id) + .orderByAsc(InvoiceReceiptSettlement::getCreateTime)); + settlements.forEach(item -> { + FormalSettlement source = formalSettlementMapper.selectById(item.getFormalSettlementId()); + if (source != null) { + item.setSettlementAmount(money(source.getSettlementAmount())); + } + item.setReceivedInvoiceAmount(receivedAmount(item.getFormalSettlementId(), id)); + }); + vo.setSettlements(settlements); + vo.setRecords(recordMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceReceiptRecord::getInvoiceReceiptId, id) + .orderByAsc(InvoiceReceiptRecord::getCreateTime))); + return vo; + } + + @Override + public List invoicePool(String keyword) { + return invoicePoolMapper.selectList(Wrappers.lambdaQuery() + .eq(KingdeeInvoicePool::getStatus, 1) + .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(KingdeeInvoicePool::getInvoiceNo, keyword) + .or().like(KingdeeInvoicePool::getIssuerName, keyword) + .or().like(KingdeeInvoicePool::getReceiverName, keyword)) + .orderByDesc(KingdeeInvoicePool::getSourceUpdatedTime) + .orderByDesc(KingdeeInvoicePool::getCreateTime) + .last("limit 200")); + } + + @Override + public List> settlementCandidates(String keyword, Long receiptId) { + List settlements = formalSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlement::getSettlementType, "payable") + .eq(FormalSettlement::getApprovalStatus, APPROVED) + .eq(FormalSettlement::getStatus, 1) + .and(Func.isNotEmpty(keyword), wrapper -> wrapper + .like(FormalSettlement::getFormalSettlementNo, keyword) + .or().like(FormalSettlement::getProjectName, keyword) + .or().like(FormalSettlement::getContractName, keyword)) + .orderByDesc(FormalSettlement::getCreateTime) + .last("limit 200")); + return settlements.stream().map(settlement -> { + BigDecimal received = receivedAmount(settlement.getId(), receiptId); + BigDecimal remaining = money(settlement.getSettlementAmount()).subtract(received).max(BigDecimal.ZERO); + Map row = new LinkedHashMap<>(); + row.put("id", settlement.getId()); + row.put("formalSettlementNo", settlement.getFormalSettlementNo()); + row.put("projectId", settlement.getProjectId()); + row.put("projectName", settlement.getProjectName()); + row.put("deptId", settlement.getDeptId()); + row.put("deptName", settlement.getDeptName()); + row.put("contractId", settlement.getContractId()); + row.put("contractNo", settlement.getContractNo()); + row.put("contractName", settlement.getContractName()); + row.put("payerName", settlement.getPayerName()); + row.put("payeeName", settlement.getPayeeName()); + row.put("settlementAmount", money(settlement.getSettlementAmount())); + row.put("receivedInvoiceAmount", received); + row.put("remainingInvoiceAmount", remaining); + return row; + }).filter(row -> ((BigDecimal) row.get("remainingInvoiceAmount")).compareTo(BigDecimal.ZERO) > 0) + .toList(); + } + + @Override + public Map referenceInformation(String settlementIds) { + List settlements = distinctIds(settlementIds).stream() + .map(this::availableSettlement) + .toList(); + assertCompatible(settlements); + FormalSettlement first = settlements.get(0); + CustomerArchive customer = findCustomer(first.getPayeeName()); + List customerEmails = customer == null ? List.of() : customerContactMapper.selectList( + Wrappers.lambdaQuery() + .eq(CustomerContact::getCustomerId, customer.getId()) + .eq(CustomerContact::getStatus, 1) + .orderByDesc(CustomerContact::getIsDefault) + .orderByAsc(CustomerContact::getCreateTime)).stream() + .map(CustomerContact::getEmail) + .filter(item -> Func.isNotEmpty(item)) + .map(String::trim) + .distinct() + .toList(); + Map result = new LinkedHashMap<>(); + result.put("projectId", first.getProjectId()); + result.put("projectName", first.getProjectName()); + result.put("deptId", first.getDeptId()); + result.put("deptName", first.getDeptName()); + result.put("payerName", first.getPayerName()); + result.put("payeeName", first.getPayeeName()); + result.put("customerEmails", customerEmails); + result.put("departmentEmails", List.of()); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(InvoiceReceiptSaveRequest request) { + validateRequest(request); + boolean creating = request.getId() == null; + InvoiceReceipt entity = creating ? new InvoiceReceipt() : editable(request.getId()); + KingdeeInvoicePool invoice = lockedInvoice(request.getKingdeeInvoicePoolId()); + assertInvoiceUnused(invoice, entity.getId()); + + Map requestedRows = distinctSettlementRows( + request.getSettlements()); + Map settlementMap = lockSettlements(requestedRows.keySet().stream().sorted().toList()); + List settlements = requestedRows.keySet().stream().map(settlementMap::get).toList(); + assertCompatible(settlements); + assertInvoiceParties(invoice, settlements.get(0)); + + BigDecimal invoiceAmount = positive(invoice.getInvoiceAmount(), "开票金额"); + BigDecimal allocatedTotal = BigDecimal.ZERO; + for (Map.Entry entry : requestedRows.entrySet()) { + FormalSettlement settlement = settlementMap.get(entry.getKey()); + BigDecimal allocated = nonNegative(entry.getValue().getAllocatedInvoiceAmount(), "分摊发票金额"); + BigDecimal received = receivedAmount(settlement.getId(), entity.getId()); + if (received.add(allocated).compareTo(money(settlement.getSettlementAmount())) > 0) { + throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + + "的累计收票金额不能超过结算总应付含税金额"); + } + allocatedTotal = allocatedTotal.add(allocated); + } + if (allocatedTotal.compareTo(invoiceAmount) != 0) { + throw new ServiceException("分摊发票金额总和必须等于发票开票金额"); + } + + if (creating) { + entity.setApprovalStatus(DRAFT); + entity.setCurrentNode("草稿"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + } + copyInvoiceInformation(entity, invoice); + FormalSettlement first = settlements.get(0); + entity.setProjectId(first.getProjectId()); + entity.setProjectName(first.getProjectName()); + entity.setDeptId(first.getDeptId()); + entity.setDeptName(first.getDeptName()); + entity.setPayerName(first.getPayerName()); + entity.setPayeeName(first.getPayeeName()); + entity.setPhone(limit(Func.isNotEmpty(request.getPhone()) ? request.getPhone() : invoice.getPhone(), + 50, "电话")); + entity.setCustomerEmails(normalizeEmails(Func.isNotEmpty(request.getCustomerEmails()) + ? request.getCustomerEmails() : invoice.getCustomerEmails(), "客户邮箱")); + entity.setDepartmentEmails(normalizeEmails(Func.isNotEmpty(request.getDepartmentEmails()) + ? request.getDepartmentEmails() : invoice.getDepartmentEmails(), "部门邮箱")); + entity.setAttachmentsJson(request.getAttachmentsJson()); + entity.setRemark(limit(request.getRemark(), 200, "备注")); + saveOrUpdate(entity); + + settlementRelationMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId())); + saveRelations(entity.getId(), requestedRows, settlementMap); + record(entity.getId(), creating ? "create" : "save", creating ? "创建草稿" : "保存草稿", + entity.getApprovalStatus(), entity.getApprovalStatus(), null, entity.getKingdeeBillNo()); + return entity.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + InvoiceReceipt entity = existing(id); + if (!DRAFT.equals(entity.getApprovalStatus())) { + throw new ServiceException("仅草稿状态的收票登记允许删除"); + } + settlementRelationMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, id)); + recordMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceReceiptRecord::getInvoiceReceiptId, id)); + removeById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(InvoiceReceiptStatusRequest request) { + InvoiceReceipt entity = editable(requiredId(request)); + validateStoredAllocation(entity); + String fromStatus = entity.getApprovalStatus(); + entity.setApprovalStatus(REVIEWING); + entity.setCurrentNode("收票审核"); + entity.setCurrentProcessor(null); + updateById(entity); + record(entity.getId(), "submit", "提交审批", fromStatus, REVIEWING, null, entity.getKingdeeBillNo()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(InvoiceReceiptStatusRequest request) { + InvoiceReceipt entity = existing(requiredId(request)); + if (!REVIEWING.equals(entity.getApprovalStatus())) { + throw new ServiceException("当前状态不允许审批通过"); + } + validateStoredAllocation(entity); + changeStatus(entity, APPROVED, "approve", "审批通过", null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void returnBill(InvoiceReceiptStatusRequest request) { + InvoiceReceipt entity = existing(requiredId(request)); + if (!REVIEWING.equals(entity.getApprovalStatus())) { + throw new ServiceException("当前状态不允许驳回"); + } + changeStatus(entity, RETURNED, "return", "已驳回", + limit(required(request.getReason(), "驳回原因"), 200, "驳回原因")); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void voidBill(InvoiceReceiptStatusRequest request) { + InvoiceReceipt entity = existing(requiredId(request)); + if (!APPROVED.equals(entity.getApprovalStatus())) { + throw new ServiceException("仅审批通过的收票登记允许作废"); + } + String reason = limit(required(request.getReason(), "作废原因"), 200, "作废原因"); + entity.setVoidReason(reason); + changeStatus(entity, VOIDED, "void", "已作废", reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String syncKingdee(Long id) { + InvoiceReceipt entity = existing(id); + if (!APPROVED.equals(entity.getApprovalStatus())) { + throw new ServiceException("仅审批通过的收票登记允许同步金蝶状态"); + } + KingdeeInvoicePool invoice = invoicePoolMapper.selectById(entity.getKingdeeInvoicePoolId()); + if (invoice == null || Objects.equals(invoice.getIsDeleted(), 1)) { + throw new ServiceException("金蝶票据池发票不存在"); + } + entity.setKingdeeBillNo(invoice.getKingdeeBillNo()); + entity.setKingdeeStatus(normalizeKingdeeStatus(invoice.getKingdeeStatus())); + updateById(entity); + record(entity.getId(), "sync", "同步金蝶状态", entity.getApprovalStatus(), + entity.getApprovalStatus(), null, entity.getKingdeeBillNo()); + return entity.getKingdeeBillNo(); + } + + private InvoiceReceiptVO toListVO(InvoiceReceipt entity) { + InvoiceReceiptVO vo = InvoiceReceiptWrapper.build().entityVO(entity); + vo.setSettlementNos(settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId()) + .orderByAsc(InvoiceReceiptSettlement::getCreateTime)).stream() + .map(InvoiceReceiptSettlement::getFormalSettlementNo) + .collect(Collectors.joining(","))); + return vo; + } + + private void validateRequest(InvoiceReceiptSaveRequest request) { + if (request == null) { + throw new ServiceException("收票登记数据不能为空"); + } + if (request.getKingdeeInvoicePoolId() == null) { + throw new ServiceException("请选择金蝶票据池发票"); + } + if (request.getSettlements() == null || request.getSettlements().isEmpty()) { + throw new ServiceException("请至少选择一张应付正式结算单"); + } + } + + private Map distinctSettlementRows( + List rows) { + Map result = new LinkedHashMap<>(); + for (InvoiceReceiptSaveRequest.SettlementRow row : rows) { + if (row == null || row.getSettlementId() == null) { + throw new ServiceException("正式结算单不能为空"); + } + if (result.putIfAbsent(row.getSettlementId(), row) != null) { + throw new ServiceException("正式结算单不能重复选择"); + } + } + return result; + } + + private Map lockSettlements(List settlementIds) { + Map result = new LinkedHashMap<>(); + for (Long settlementId : settlementIds) { + FormalSettlement settlement = formalSettlementMapper.selectOne( + Wrappers.lambdaQuery() + .eq(FormalSettlement::getId, settlementId) + .last("FOR UPDATE")); + result.put(settlementId, validateSettlement(settlement)); + } + return result; + } + + private FormalSettlement availableSettlement(Long id) { + if (id == null) { + throw new ServiceException("正式结算单不能为空"); + } + return validateSettlement(formalSettlementMapper.selectById(id)); + } + + private FormalSettlement validateSettlement(FormalSettlement settlement) { + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) { + throw new ServiceException("正式结算单不存在"); + } + if (!Objects.equals(settlement.getStatus(), 1) + || !APPROVED.equals(settlement.getApprovalStatus()) + || !"payable".equals(settlement.getSettlementType())) { + throw new ServiceException("只能选择审批通过、未作废的应付正式结算单"); + } + return settlement; + } + + private KingdeeInvoicePool lockedInvoice(Long id) { + KingdeeInvoicePool invoice = invoicePoolMapper.selectOne(Wrappers.lambdaQuery() + .eq(KingdeeInvoicePool::getId, id) + .last("FOR UPDATE")); + if (invoice == null || Objects.equals(invoice.getIsDeleted(), 1) + || !Objects.equals(invoice.getStatus(), 1)) { + throw new ServiceException("金蝶票据池发票不存在或已失效"); + } + required(invoice.getInvoiceNo(), "发票号码"); + return invoice; + } + + private void assertInvoiceUnused(KingdeeInvoicePool invoice, Long excludeReceiptId) { + long count = count(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(InvoiceReceipt::getKingdeeInvoicePoolId, invoice.getId()) + .or().eq(InvoiceReceipt::getInvoiceNo, invoice.getInvoiceNo())) + .ne(excludeReceiptId != null, InvoiceReceipt::getId, excludeReceiptId)); + if (count > 0) { + throw new ServiceException("发票" + invoice.getInvoiceNo() + "已登记,不能重复收票"); + } + } + + private void assertCompatible(List settlements) { + if (settlements.isEmpty()) { + throw new ServiceException("请选择应付正式结算单"); + } + FormalSettlement first = settlements.get(0); + if (settlements.stream().anyMatch(item -> !Objects.equals(first.getProjectId(), item.getProjectId()) + || !Objects.equals(first.getDeptId(), item.getDeptId()) + || !Objects.equals(first.getPayerName(), item.getPayerName()) + || !Objects.equals(first.getPayeeName(), item.getPayeeName()))) { + throw new ServiceException("关联结算单必须属于同一项目、组织及收付款方"); + } + } + + private void assertInvoiceParties(KingdeeInvoicePool invoice, FormalSettlement settlement) { + if (!sameName(invoice.getReceiverName(), settlement.getPayerName()) + || !sameName(invoice.getIssuerName(), settlement.getPayeeName())) { + throw new ServiceException("金蝶发票的开票单位、受票单位与结算单收付款方不一致"); + } + } + + private boolean sameName(String first, String second) { + return Func.isNotEmpty(first) && Func.isNotEmpty(second) && first.trim().equals(second.trim()); + } + + private BigDecimal receivedAmount(Long settlementId, Long excludeReceiptId) { + return activeRelations(settlementId, excludeReceiptId).stream() + .map(InvoiceReceiptSettlement::getAllocatedInvoiceAmount) + .map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private List activeRelations(Long settlementId, Long excludeReceiptId) { + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getFormalSettlementId, settlementId) + .ne(excludeReceiptId != null, InvoiceReceiptSettlement::getInvoiceReceiptId, excludeReceiptId)); + if (relations.isEmpty()) { + return List.of(); + } + Map receipts = listByIds(relations.stream() + .map(InvoiceReceiptSettlement::getInvoiceReceiptId) + .distinct() + .toList()).stream().collect(Collectors.toMap(InvoiceReceipt::getId, Function.identity())); + return relations.stream().filter(relation -> { + InvoiceReceipt receipt = receipts.get(relation.getInvoiceReceiptId()); + return receipt != null && !VOIDED.equals(receipt.getApprovalStatus()) + && !Objects.equals(receipt.getIsDeleted(), 1); + }).toList(); + } + + private void saveRelations(Long receiptId, + Map rows, + Map settlementMap) { + for (Map.Entry entry : rows.entrySet()) { + FormalSettlement source = settlementMap.get(entry.getKey()); + InvoiceReceiptSettlement relation = new InvoiceReceiptSettlement(); + relation.setInvoiceReceiptId(receiptId); + relation.setFormalSettlementId(source.getId()); + relation.setFormalSettlementNo(source.getFormalSettlementNo()); + relation.setSettlementAmount(money(source.getSettlementAmount())); + relation.setReceivedInvoiceAmount(receivedAmount(source.getId(), receiptId)); + relation.setAllocatedInvoiceAmount(entry.getValue().getAllocatedInvoiceAmount()); + settlementRelationMapper.insert(relation); + } + } + + private void validateStoredAllocation(InvoiceReceipt entity) { + KingdeeInvoicePool invoice = lockedInvoice(entity.getKingdeeInvoicePoolId()); + assertInvoiceUnused(invoice, entity.getId()); + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId()) + .orderByAsc(InvoiceReceiptSettlement::getCreateTime)); + if (relations.isEmpty()) { + throw new ServiceException("请至少选择一张应付正式结算单"); + } + Map settlementMap = lockSettlements(relations.stream() + .map(InvoiceReceiptSettlement::getFormalSettlementId) + .distinct() + .sorted() + .toList()); + List settlements = relations.stream() + .map(item -> settlementMap.get(item.getFormalSettlementId())) + .toList(); + assertCompatible(settlements); + assertInvoiceParties(invoice, settlements.get(0)); + BigDecimal allocatedTotal = BigDecimal.ZERO; + for (InvoiceReceiptSettlement relation : relations) { + FormalSettlement settlement = settlementMap.get(relation.getFormalSettlementId()); + BigDecimal allocated = nonNegative(relation.getAllocatedInvoiceAmount(), "分摊发票金额"); + BigDecimal received = receivedAmount(settlement.getId(), entity.getId()); + if (received.add(allocated).compareTo(money(settlement.getSettlementAmount())) > 0) { + throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + + "的累计收票金额不能超过结算总应付含税金额"); + } + allocatedTotal = allocatedTotal.add(allocated); + } + if (allocatedTotal.compareTo(positive(invoice.getInvoiceAmount(), "开票金额")) != 0) { + throw new ServiceException("分摊发票金额总和必须等于发票开票金额"); + } + } + + private void copyInvoiceInformation(InvoiceReceipt target, KingdeeInvoicePool source) { + target.setKingdeeInvoicePoolId(source.getId()); + target.setInvoiceNo(limit(source.getInvoiceNo(), 32, "发票号码")); + target.setInvoiceDate(source.getInvoiceDate()); + target.setInvoiceType(source.getInvoiceType()); + target.setTaxRate(nonNegative(source.getTaxRate(), "税率")); + target.setInvoiceAmount(positive(source.getInvoiceAmount(), "开票金额")); + target.setTaxAmount(nonNegative(source.getTaxAmount(), "税额")); + target.setReceiverName(limit(required(source.getReceiverName(), "受票单位"), 100, "受票单位")); + target.setIssuerName(limit(required(source.getIssuerName(), "开票单位"), 100, "开票单位")); + target.setBankName(source.getBankName()); + target.setBankAccount(source.getBankAccount()); + target.setIssuingBank(source.getIssuingBank()); + target.setKingdeeBillNo(source.getKingdeeBillNo()); + target.setKingdeeStatus(normalizeKingdeeStatus(source.getKingdeeStatus())); + } + + private String normalizeKingdeeStatus(String status) { + if ("synced".equals(status) || "failed".equals(status)) { + return status; + } + return "unsynced"; + } + + private void changeStatus(InvoiceReceipt entity, String toStatus, String actionType, + String actionName, String reason) { + String fromStatus = entity.getApprovalStatus(); + entity.setApprovalStatus(toStatus); + entity.setCurrentNode(actionName); + entity.setCurrentProcessor(AuthUtil.getUserName()); + updateById(entity); + record(entity.getId(), actionType, actionName, fromStatus, toStatus, reason, entity.getKingdeeBillNo()); + } + + private void record(Long receiptId, String actionType, String actionName, String fromStatus, + String toStatus, String reason, String kingdeeBillNo) { + InvoiceReceiptRecord record = new InvoiceReceiptRecord(); + record.setInvoiceReceiptId(receiptId); + record.setActionType(actionType); + record.setActionName(actionName); + record.setFromStatus(fromStatus); + record.setToStatus(toStatus); + record.setOperatorName(AuthUtil.getUserName()); + record.setReason(reason); + record.setKingdeeBillNo(kingdeeBillNo); + recordMapper.insert(record); + } + + private InvoiceReceipt existing(Long id) { + if (id == null) { + throw new ServiceException("收票登记ID不能为空"); + } + InvoiceReceipt entity = getById(id); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) { + throw new ServiceException("收票登记不存在"); + } + return entity; + } + + private InvoiceReceipt editable(Long id) { + InvoiceReceipt entity = existing(id); + if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) { + throw new ServiceException("当前状态不可编辑"); + } + return entity; + } + + private Long requiredId(InvoiceReceiptStatusRequest request) { + if (request == null || request.getId() == null) { + throw new ServiceException("收票登记ID不能为空"); + } + return request.getId(); + } + + private List distinctIds(String ids) { + return ids == null ? List.of() : Func.toLongList(ids).stream().distinct().toList(); + } + + private CustomerArchive findCustomer(String name) { + if (Func.isEmpty(name)) { + return null; + } + return customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, name) + .or().eq(CustomerArchive::getShortName, name)) + .eq(CustomerArchive::getStatus, 1) + .last("limit 1")); + } + + private String normalizeEmails(String value, String name) { + if (Func.isEmpty(value)) { + return ""; + } + List emails = List.of(value.split("[;,,;]")).stream() + .map(String::trim) + .filter(Func::isNotEmpty) + .distinct() + .toList(); + if (emails.size() > 3) { + throw new ServiceException(name + "最多填写3个"); + } + emails.forEach(email -> { + if (email.length() > 100 || !email.matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) { + throw new ServiceException(name + "格式不正确"); + } + }); + return limit(String.join(";", emails), 200, name); + } + + private String required(String value, String name) { + if (Func.isEmpty(value)) { + throw new ServiceException(name + "不能为空"); + } + return value.trim(); + } + + private String limit(String value, int length, String name) { + if (value != null && value.length() > length) { + throw new ServiceException(name + "不能超过" + length + "个字符"); + } + return value; + } + + private BigDecimal nonNegative(BigDecimal value, String name) { + if (value == null) { + throw new ServiceException(name + "不能为空"); + } + if (value.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException(name + "不能小于0"); + } + return value; + } + + private BigDecimal positive(BigDecimal value, String name) { + BigDecimal result = nonNegative(value, name); + if (result.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException(name + "必须大于0"); + } + return result; + } + + private BigDecimal money(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java new file mode 100644 index 0000000..3ac1348 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -0,0 +1,392 @@ +/** + * 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.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.BillLedgerMapper; +import org.springblade.transport.mapper.BillLedgerUsageMapper; +import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.ProjectApplyMapper; +import org.springblade.transport.mapper.CustomerArchiveMapper; +import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper; +import org.springblade.transport.mapper.PaymentApplicationMapper; +import org.springblade.transport.mapper.PaymentApplicationRecordMapper; +import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest; +import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; +import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.BillLedger; +import org.springblade.transport.pojo.entity.BillLedgerUsage; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.ProjectApply; +import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springblade.transport.pojo.entity.PaymentApplicationInvoice; +import org.springblade.transport.pojo.vo.PaymentApplicationVO; +import org.springblade.transport.service.IPaymentApplicationService; +import org.springblade.transport.wrapper.PaymentApplicationWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; + +/** 付款申请服务实现。 @author Chill */ +@Service +@RequiredArgsConstructor +public class PaymentApplicationServiceImpl extends BaseServiceImpl + implements IPaymentApplicationService { + private static final String DRAFT = "draft"; + private static final String REVIEWING = "reviewing"; + private static final String APPROVED = "approved"; + private static final String RETURNED = "returned"; + private static final String VOIDED = "voided"; + private final PaymentApplicationInvoiceMapper invoiceMapper; + private final PaymentApplicationRecordMapper recordMapper; + private final PreSettlementMapper preSettlementMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final ProjectApplyMapper projectApplyMapper; + private final CustomerArchiveMapper customerArchiveMapper; + private final BillLedgerMapper billLedgerMapper; + private final BillLedgerUsageMapper billLedgerUsageMapper; + + @Override + public IPage selectPage(IPage page, PaymentApplicationVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getPaymentNo()), PaymentApplication::getPaymentNo, query.getPaymentNo()) + .like(Func.isNotEmpty(query.getPayeeName()), PaymentApplication::getPayeeName, query.getPayeeName()) + .like(Func.isNotEmpty(query.getProjectName()), PaymentApplication::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), PaymentApplication::getDeptName, query.getDeptName()) + .like(Func.isNotEmpty(query.getSettlementNo()), PaymentApplication::getSettlementNo, query.getSettlementNo()) + .eq(Func.isNotEmpty(query.getPaymentType()), PaymentApplication::getPaymentType, query.getPaymentType()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), PaymentApplication::getApprovalStatus, query.getApprovalStatus()) + .eq(Func.isNotEmpty(query.getKingdeeStatus()), PaymentApplication::getKingdeeStatus, query.getKingdeeStatus()) + .ge(query.getApplyStartDate() != null, PaymentApplication::getApplyDate, query.getApplyStartDate()) + .le(query.getApplyEndDate() != null, PaymentApplication::getApplyDate, query.getApplyEndDate()) + .orderByDesc(PaymentApplication::getCreateTime); + return page(page, wrapper).convert(item -> { + PaymentApplicationVO vo = PaymentApplicationWrapper.build().entityVO(item); + return vo; + }); + } + + @Override + public PaymentApplicationVO detail(Long id) { + PaymentApplication entity = existing(id); + PaymentApplicationVO vo = PaymentApplicationWrapper.build().entityVO(entity); + vo.setInvoices(invoiceMapper.selectList(Wrappers.lambdaQuery() + .eq(PaymentApplicationInvoice::getPaymentApplicationId, id).orderByAsc(PaymentApplicationInvoice::getLineNo))); + vo.setPaymentRecords(recordMapper.selectList(Wrappers.lambdaQuery() + .eq(org.springblade.transport.pojo.entity.PaymentApplicationRecord::getPaymentApplicationId, id) + .orderByDesc(org.springblade.transport.pojo.entity.PaymentApplicationRecord::getCreateTime))); + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(PaymentApplicationSaveRequest request) { + PaymentApplication entity = request.getId() == null ? new PaymentApplication() : editable(request.getId()); + validateRequest(request); + if (entity.getId() == null) { + entity.setPaymentNo(nextNo()); + entity.setApprovalStatus(DRAFT); + entity.setCurrentNode("草稿"); + entity.setKingdeeStatus("unsynced"); + entity.setPaidAmount(BigDecimal.ZERO); + entity.setInvoiceStatus("unmatched"); + } + entity.setPaymentType(request.getPaymentType()); + entity.setPaymentMethod(request.getPaymentMethod()); + entity.setPaymentRatio(request.getPaymentRatio()); + entity.setAppliedAmount(nonNegative(request.getAppliedAmount(), "申请付款金额")); + entity.setReceiptAccountId(request.getReceiptAccountId()); + entity.setReceiptAccountName(request.getReceiptAccountName()); + entity.setBankName(request.getBankName()); + entity.setBankAccount(request.getBankAccount()); + entity.setAttachmentsJson(request.getAttachmentsJson()); + entity.setRemark(limit(request.getRemark(), 200)); + entity.setApplyDate(entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()); + entity.setApplicantName(Func.isEmpty(entity.getApplicantName()) ? AuthUtil.getUserName() : entity.getApplicantName()); + fillReference(entity, request); + fillBillLedger(entity, request); + validateQuota(entity); + saveOrUpdate(entity); + invoiceMapper.delete(Wrappers.lambdaQuery() + .eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId())); + int lineNo = 1; + BigDecimal matchedInvoiceAmount = BigDecimal.ZERO; + for (PaymentApplicationInvoiceRequest item : request.getInvoices() == null ? List.of() : request.getInvoices()) { + validateInvoice(item); + PaymentApplicationInvoice invoice = Objects.requireNonNull(BeanUtil.copyProperties(item, PaymentApplicationInvoice.class)); + invoice.setId(null); + invoice.setPaymentApplicationId(entity.getId()); + invoice.setLineNo(lineNo++); + invoiceMapper.insert(invoice); + matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount())); + } + if (matchedInvoiceAmount.compareTo(money(entity.getAppliedAmount())) > 0) { + throw new ServiceException("发票匹配金额合计不能超过申请付款金额"); + } + entity.setMatchedInvoiceAmount(matchedInvoiceAmount); + entity.setInvoiceStatus(matchedInvoiceAmount.compareTo(BigDecimal.ZERO) > 0 ? "matched" : "unmatched"); + updateById(entity); + return entity.getId(); + } + + @Override @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); removeById(entity); } + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(PaymentApplicationStatusRequest request) { + PaymentApplication entity = lockedPayment(request.getId()); + if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) { + throw new ServiceException("当前状态不允许提交"); + } + validateSelectedBill(entity, false); + entity.setApprovalStatus(REVIEWING); + entity.setCurrentNode("财务审核"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + updateById(entity); + } + @Override public void returnBill(PaymentApplicationStatusRequest request) { change(request.getId(), REVIEWING, RETURNED, "已驳回", request.getReason()); } + @Override + @Transactional(rollbackFor = Exception.class) + public void voidBill(PaymentApplicationStatusRequest request) { + PaymentApplication entity = lockedPayment(request.getId()); + if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许作废"); + if (isBillPayment(entity.getPaymentMethod())) releaseBillBalance(entity); + entity.setApprovalStatus(VOIDED); + entity.setCurrentNode("已作废"); + entity.setCurrentProcessor(AuthUtil.getUserName()); + entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200)); + updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(PaymentApplicationStatusRequest request) { + PaymentApplication entity = lockedPayment(request.getId()); + if (!REVIEWING.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批中的付款申请允许审核"); + if (isBillPayment(entity.getPaymentMethod())) useBillBalance(entity); + entity.setApprovalStatus(APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName()); + updateById(entity); + } + + @Override + public String syncKingdee(Long id) { + PaymentApplication entity = existing(id); + if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许生成金蝶单据"); + if ("synced".equals(entity.getKingdeeStatus())) return entity.getKingdeeBillNo(); + String no = "FKD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + String.format("%05d", count(Wrappers.lambdaQuery().likeRight(PaymentApplication::getKingdeeBillNo, "FKD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE))) + 1); + entity.setKingdeeBillNo(no); entity.setKingdeeStatus("synced"); updateById(entity); return no; + } + + private void validateRequest(PaymentApplicationSaveRequest request) { + if (Func.isEmpty(request.getPaymentType())) throw new ServiceException("付款类型不能为空"); + if (!List.of("project_advance", "progress_advance", "settlement_payment").contains(request.getPaymentType())) throw new ServiceException("付款类型不合法"); + if (Func.isEmpty(request.getPaymentMethod())) throw new ServiceException("付款方式不能为空"); + if (!List.of("bank_transfer", "bank_draft", "commercial_draft").contains(request.getPaymentMethod())) throw new ServiceException("付款方式不合法"); + if (isBillPayment(request.getPaymentMethod()) && request.getBillLedgerId() == null) throw new ServiceException("汇票付款必须选择汇票台账"); + if (request.getPaymentRatio() != null && (request.getPaymentRatio().compareTo(BigDecimal.ZERO) < 0 || request.getPaymentRatio().compareTo(BigDecimal.valueOf(100)) > 0)) throw new ServiceException("付款比例必须在0-100之间"); + if (request.getAppliedAmount() == null || request.getAppliedAmount().compareTo(BigDecimal.ZERO) < 0) throw new ServiceException("申请付款金额不能小于0"); + if (!"project_advance".equals(request.getPaymentType()) && request.getSettlementId() == null && request.getPreSettlementId() == null) throw new ServiceException("非项目预付必须关联结算单"); + if ("project_advance".equals(request.getPaymentType()) && request.getProjectId() == null) throw new ServiceException("项目预付必须选择所属项目"); + if ("progress_advance".equals(request.getPaymentType()) && request.getPreSettlementId() == null) throw new ServiceException("进度预付必须关联预结算单"); + if ("settlement_payment".equals(request.getPaymentType()) && request.getSettlementId() == null) throw new ServiceException("结算付款必须关联正式结算单"); + } + + private void validateInvoice(PaymentApplicationInvoiceRequest invoice) { + BigDecimal invoiceAmount = money(invoice.getInvoiceAmount()); + BigDecimal matchedAmount = money(invoice.getMatchedAmount()); + if (invoiceAmount.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("发票金额不能小于0"); + } + if (matchedAmount.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("发票匹配金额不能小于0"); + } + if (matchedAmount.compareTo(invoiceAmount) > 0) { + throw new ServiceException("单张发票匹配金额不能超过发票金额"); + } + if (invoice.getTaxRate() != null && (invoice.getTaxRate().compareTo(BigDecimal.ZERO) < 0 + || invoice.getTaxRate().compareTo(BigDecimal.valueOf(100)) > 0)) { + throw new ServiceException("发票税率必须在0-100之间"); + } + } + + private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) { + if (request.getSettlementId() != null) { + FormalSettlement source = formalSettlementMapper.selectById(request.getSettlementId()); + if (source == null) throw new ServiceException("正式结算单不存在"); + if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单"); + entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null); + entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(money(source.getAppliedPaymentAmount()))); entity.setBillType("正式结算单"); + } else if (request.getPreSettlementId() != null) { + PreSettlement source = preSettlementMapper.selectById(request.getPreSettlementId()); + if (source == null) throw new ServiceException("预结算单不存在"); + if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的预结算单"); + entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(money(source.getAdvanceAppliedAmount()))); entity.setBillType("预结算单"); + } else { + entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType()); + } + if (money(entity.getAppliedAmount()).compareTo(money(entity.getPayableAmount())) > 0 && money(entity.getPayableAmount()).compareTo(BigDecimal.ZERO) > 0) throw new ServiceException("申请付款金额不能超过可付款金额"); + } + + private void validateQuota(PaymentApplication entity) { + BigDecimal usedByProject = sumApplied(Wrappers.lambdaQuery() + .eq(entity.getProjectId() != null, PaymentApplication::getProjectId, entity.getProjectId()) + .ne(entity.getId() != null, PaymentApplication::getId, entity.getId())); + ProjectApply project = entity.getProjectId() == null ? null : projectApplyMapper.selectById(entity.getProjectId()); + if (project != null && project.getFundLimit() != null) { + BigDecimal projectLimit = project.getFundLimit().multiply(BigDecimal.valueOf(10000)); + if (usedByProject.add(money(entity.getAppliedAmount())).compareTo(projectLimit) > 0) throw new ServiceException("申请付款金额超过项目剩余资金使用额度"); + } + if (Func.isNotEmpty(entity.getPayeeName())) { + CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, entity.getPayeeName()).or().eq(CustomerArchive::getShortName, entity.getPayeeName())) + .eq(CustomerArchive::getIsDeleted, 0).last("limit 1")); + if (customer != null && customer.getMaxCreditLimit() != null) { + BigDecimal customerLimit = customer.getMaxCreditLimit().multiply(BigDecimal.valueOf(10000)); + BigDecimal usedByCustomer = sumApplied(Wrappers.lambdaQuery() + .eq(PaymentApplication::getPayeeName, entity.getPayeeName()).ne(entity.getId() != null, PaymentApplication::getId, entity.getId())); + if (usedByCustomer.add(money(entity.getAppliedAmount())).compareTo(customerLimit) > 0) throw new ServiceException("申请付款金额超过客户剩余资金使用额度"); + } + } + } + + private BigDecimal sumApplied(LambdaQueryWrapper wrapper) { + return list(wrapper.ne(PaymentApplication::getApprovalStatus, VOIDED)).stream() + .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private void fillBillLedger(PaymentApplication entity, PaymentApplicationSaveRequest request) { + if (!isBillPayment(request.getPaymentMethod())) { + entity.setBillLedgerId(null); + entity.setBillNo(null); + return; + } + BillLedger ledger = billLedgerMapper.selectById(request.getBillLedgerId()); + validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true); + entity.setBillLedgerId(ledger.getId()); + entity.setBillNo(ledger.getBillNo()); + } + + private void validateSelectedBill(PaymentApplication entity, boolean locked) { + if (!isBillPayment(entity.getPaymentMethod())) return; + BillLedger ledger = locked ? lockedBill(entity.getBillLedgerId()) : billLedgerMapper.selectById(entity.getBillLedgerId()); + validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true); + } + + private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity) { + if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在"); + if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) { + throw new ServiceException("所选汇票已到期"); + } + if (money(amount).compareTo(money(ledger.getAvailableBalance())) > 0) throw new ServiceException("申请付款金额超过汇票可用余额"); + if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内"); + } + + private void useBillBalance(PaymentApplication entity) { + BillLedger ledger = lockedBill(entity.getBillLedgerId()); + validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true); + BillLedgerUsage exists = billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()).last("FOR UPDATE")); + if (exists != null) throw new ServiceException("该付款申请已生成汇票使用记录"); + BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP); + ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount)); + billLedgerMapper.updateById(ledger); + BillLedgerUsage usage = new BillLedgerUsage(); + usage.setBillLedgerId(ledger.getId()); + usage.setPaymentApplicationId(entity.getId()); + usage.setApplicationNo(entity.getPaymentNo()); + usage.setUsedAmount(amount); + usage.setUseDeptId(entity.getDeptId()); + usage.setUseDeptName(entity.getDeptName()); + usage.setUsageStatus(APPROVED); + usage.setStatus(1); + billLedgerUsageMapper.insert(usage); + } + + private void releaseBillBalance(PaymentApplication entity) { + BillLedgerUsage usage = billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()) + .eq(BillLedgerUsage::getUsageStatus, APPROVED).last("FOR UPDATE")); + if (usage == null) throw new ServiceException("未找到对应的汇票使用记录,无法作废"); + BillLedger ledger = lockedBill(usage.getBillLedgerId()); + ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount())) + .min(money(ledger.getFaceAmount()))); + billLedgerMapper.updateById(ledger); + usage.setUsageStatus("released"); + billLedgerUsageMapper.updateById(usage); + } + + private BillLedger lockedBill(Long id) { + if (id == null) throw new ServiceException("汇票台账不能为空"); + BillLedger ledger = billLedgerMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedger::getId, id).last("FOR UPDATE")); + if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在"); + return ledger; + } + + private PaymentApplication lockedPayment(Long id) { + PaymentApplication entity = baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(PaymentApplication::getId, id).last("FOR UPDATE")); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("付款申请不存在"); + return entity; + } + + private boolean departmentAvailable(BillLedger ledger, Long deptId) { + if (Func.isEmpty(ledger.getAvailableDeptIdsJson())) return false; + try { + Object parsed = JsonUtil.parse(ledger.getAvailableDeptIdsJson(), List.class); + if (!(parsed instanceof List values)) return false; + if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true; + return deptId != null && values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value))); + } catch (Exception exception) { + throw new ServiceException("汇票可用部门配置不正确"); + } + } + + private boolean isBillPayment(String paymentMethod) { + return List.of("bank_draft", "commercial_draft").contains(paymentMethod); + } + + private void change(Long id, String from, String to, String node, String reason) { PaymentApplication entity = existing(id); if (!from.equals(entity.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作"); entity.setApprovalStatus(to); entity.setCurrentNode(node); entity.setCurrentProcessor(AuthUtil.getUserName()); entity.setRemark(reason == null ? entity.getRemark() : limit(reason, 200)); updateById(entity); } + private PaymentApplication existing(Long id) { PaymentApplication entity = getById(id); if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("付款申请不存在"); return entity; } + private PaymentApplication editable(Long id) { PaymentApplication entity = existing(id); if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) throw new ServiceException("当前状态不可编辑"); return entity; } + private String nextNo() { String prefix = "FKD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); return prefix + String.format("%05d", count(Wrappers.lambdaQuery().likeRight(PaymentApplication::getPaymentNo, prefix)) + 1); } + private BigDecimal nonNegative(BigDecimal value, String name) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(name + "不能小于0"); return value; } + private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private String limit(String value, int length) { if (value != null && value.length() > length) throw new ServiceException("备注不能超过" + length + "个字符"); return value; } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java new file mode 100644 index 0000000..04d09a0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java @@ -0,0 +1,259 @@ +/** + * 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.transport.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.KingdeeReceiptFlowMapper; +import org.springblade.transport.mapper.ReceiptClaimMapper; +import org.springblade.transport.mapper.ReceiptClaimSettlementMapper; +import org.springblade.transport.mapper.ReceiptFlowRecordMapper; +import org.springblade.transport.pojo.dto.ReceiptClaimAttachmentsRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springblade.transport.pojo.entity.ReceiptClaim; +import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; +import org.springblade.transport.pojo.entity.ReceiptFlowRecord; +import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO; +import org.springblade.transport.service.IReceiptClaimRecordService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * 认领记录服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl + implements IReceiptClaimRecordService { + + private static final String CLAIMED = "claimed"; + private static final String VOIDED = "voided"; + private static final String APPROVED = "approved"; + + private final ReceiptClaimSettlementMapper claimSettlementMapper; + private final KingdeeReceiptFlowMapper receiptFlowMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final ReceiptFlowRecordMapper recordMapper; + + @Override + public IPage selectPage(IPage page, + ReceiptClaimRecordVO query) { + page.setRecords(baseMapper.selectClaimRecordPage(page, query, AuthUtil.getUserId())); + page.getRecords().forEach(this::fillStatusNames); + return page; + } + + @Override + public ReceiptClaimRecordVO detail(Long id) { + if (id == null) { + throw new ServiceException("认领记录ID不能为空"); + } + ReceiptClaimRecordVO record = baseMapper.selectClaimRecordDetail(id, AuthUtil.getUserId()); + if (record == null) { + throw new ServiceException("认领记录不存在或无权查看"); + } + record.setSettlements(claimSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getReceiptClaimId, id) + .orderByAsc(ReceiptClaimSettlement::getCreateTime))); + fillStatusNames(record); + return record; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAttachments(ReceiptClaimAttachmentsRequest request) { + if (request == null || request.getId() == null) { + throw new ServiceException("认领记录ID不能为空"); + } + if (request.getAttachmentsJson() != null && request.getAttachmentsJson().length() > 2000000) { + throw new ServiceException("附件信息不能超过2MB"); + } + ReceiptClaim claim = baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(ReceiptClaim::getId, request.getId()) + .eq(ReceiptClaim::getClaimerId, AuthUtil.getUserId()) + .last("FOR UPDATE")); + if (claim == null || Objects.equals(claim.getIsDeleted(), 1)) { + throw new ServiceException("认领记录不存在或无权操作"); + } + if (!CLAIMED.equals(normalizeClaimStatus(claim.getClaimStatus()))) { + throw new ServiceException("已作废认领记录不允许修改附件"); + } + claim.setAttachmentsJson(request.getAttachmentsJson()); + updateById(claim); + + ReceiptFlowRecord operationRecord = new ReceiptFlowRecord(); + operationRecord.setReceiptFlowId(claim.getReceiptFlowId()); + operationRecord.setReceiptClaimId(claim.getId()); + operationRecord.setActionType("update_claim_attachments"); + operationRecord.setActionName("维护认领记录附件"); + operationRecord.setFromStatus(CLAIMED); + operationRecord.setToStatus(CLAIMED); + operationRecord.setOperationAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + operationRecord.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" + : AuthUtil.getUserName()); + operationRecord.setContent("更新认领记录附件"); + recordMapper.insert(operationRecord); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String voidClaim(Long id) { + ReceiptClaim claim = baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(ReceiptClaim::getId, id) + .eq(ReceiptClaim::getClaimerId, AuthUtil.getUserId()) + .last("FOR UPDATE")); + if (claim == null || Objects.equals(claim.getIsDeleted(), 1)) { + throw new ServiceException("认领记录不存在或无权作废"); + } + if (!CLAIMED.equals(normalizeClaimStatus(claim.getClaimStatus()))) { + throw new ServiceException("仅已认领记录允许作废"); + } + + KingdeeReceiptFlow flow = receiptFlowMapper.selectOne( + Wrappers.lambdaQuery() + .eq(KingdeeReceiptFlow::getId, claim.getReceiptFlowId()) + .last("FOR UPDATE")); + if (flow == null || Objects.equals(flow.getIsDeleted(), 1)) { + throw new ServiceException("关联收款流水不存在"); + } + + List relations = claimSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getReceiptClaimId, claim.getId()) + .eq(ReceiptClaimSettlement::getStatus, 1)); + if (relations.isEmpty()) { + throw new ServiceException("认领记录不存在有效结算分摊"); + } + + List orderedRelations = relations.stream() + .sorted(Comparator.comparing(ReceiptClaimSettlement::getFormalSettlementId)) + .toList(); + for (ReceiptClaimSettlement relation : orderedRelations) { + FormalSettlement settlement = formalSettlementMapper.selectOne( + Wrappers.lambdaQuery() + .eq(FormalSettlement::getId, relation.getFormalSettlementId()) + .last("FOR UPDATE")); + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) { + throw new ServiceException("关联结算单不存在"); + } + BigDecimal paidAfter = money(settlement.getPaidAmount()) + .subtract(money(relation.getAllocatedReceiptAmount())).max(BigDecimal.ZERO); + settlement.setPaidAmount(paidAfter); + settlement.setPaymentStatus(amountStatus(paidAfter, settlement.getSettlementAmount())); + formalSettlementMapper.updateById(settlement); + relation.setStatus(0); + claimSettlementMapper.updateById(relation); + } + + BigDecimal flowClaimedAfter = money(flow.getClaimedAmount()) + .subtract(money(claim.getClaimAmount())).max(BigDecimal.ZERO); + flow.setClaimedAmount(flowClaimedAfter); + flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getReceiptAmount())); + receiptFlowMapper.updateById(flow); + + String kingdeeBillNo = buildKingdeeBillNo(claim.getId()); + claim.setClaimStatus(VOIDED); + claim.setKingdeeBillNo(kingdeeBillNo); + claim.setKingdeeBillStatus(APPROVED); + claim.setVoidedBy(AuthUtil.getUserId()); + claim.setVoidedByName(Func.isEmpty(AuthUtil.getUserName()) ? claim.getClaimerName() + : AuthUtil.getUserName()); + claim.setVoidedTime(LocalDateTime.now()); + updateById(claim); + + ReceiptFlowRecord operationRecord = new ReceiptFlowRecord(); + operationRecord.setReceiptFlowId(flow.getId()); + operationRecord.setReceiptClaimId(claim.getId()); + operationRecord.setActionType("void_claim"); + operationRecord.setActionName("作废认领记录"); + operationRecord.setFromStatus(CLAIMED); + operationRecord.setToStatus(VOIDED); + operationRecord.setOperationAmount(money(claim.getClaimAmount())); + operationRecord.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" + : AuthUtil.getUserName()); + operationRecord.setContent("生成金蝶认领冲单:" + kingdeeBillNo + ",状态:审核通过"); + recordMapper.insert(operationRecord); + return kingdeeBillNo; + } + + private void fillStatusNames(ReceiptClaimRecordVO record) { + record.setClaimStatusName(VOIDED.equals(normalizeClaimStatus(record.getClaimStatus())) + ? "已作废" : "已认领"); + record.setKingdeeBillStatusName(switch (record.getKingdeeBillStatus() == null + ? "" : record.getKingdeeBillStatus()) { + case APPROVED -> "审核通过"; + case "failed" -> "处理失败"; + default -> "未生成"; + }); + } + + private String normalizeClaimStatus(String claimStatus) { + return VOIDED.equals(claimStatus) ? VOIDED : CLAIMED; + } + + private String buildKingdeeBillNo(Long claimId) { + String time = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + String suffix = String.valueOf(claimId); + return "KDCX" + time + suffix.substring(Math.max(0, suffix.length() - 6)); + } + + private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal receiptAmount) { + if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) { + return "unclaimed"; + } + return claimedAmount.compareTo(money(receiptAmount)) >= 0 ? "claimed" : "partial"; + } + + private String amountStatus(BigDecimal paidAmount, BigDecimal settlementAmount) { + if (paidAmount.compareTo(BigDecimal.ZERO) <= 0) { + return "unpaid"; + } + return paidAmount.compareTo(money(settlementAmount)) >= 0 ? "paid" : "partial"; + } + + private BigDecimal money(BigDecimal amount) { + return amount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) + : amount.setScale(2, RoundingMode.HALF_UP); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java new file mode 100644 index 0000000..9d4e59a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java @@ -0,0 +1,504 @@ +/** + * 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.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.SysCache; +import org.springblade.system.cache.UserCache; +import org.springblade.system.pojo.entity.Dept; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.KingdeeReceiptFlowMapper; +import org.springblade.transport.mapper.ReceiptClaimMapper; +import org.springblade.transport.mapper.ReceiptClaimSettlementMapper; +import org.springblade.transport.mapper.ReceiptFlowRecordMapper; +import org.springblade.transport.pojo.dto.ReceiptClaimRequest; +import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springblade.transport.pojo.entity.ReceiptClaim; +import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; +import org.springblade.transport.pojo.entity.ReceiptFlowRecord; +import org.springblade.transport.pojo.vo.ReceiptFlowVO; +import org.springblade.transport.service.IReceiptFlowService; +import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.wrapper.ReceiptFlowWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 收款流水服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class ReceiptFlowServiceImpl extends BaseServiceImpl + implements IReceiptFlowService { + + private static final String UNCLAIMED = "unclaimed"; + private static final String PARTIAL = "partial"; + private static final String CLAIMED = "claimed"; + private static final String APPROVED = "approved"; + private static final String RECEIVABLE = "receivable"; + + private final ReceiptClaimMapper claimMapper; + private final ReceiptClaimSettlementMapper claimSettlementMapper; + private final ReceiptFlowRecordMapper recordMapper; + private final FormalSettlementMapper formalSettlementMapper; + + @Override + public IPage selectPage(IPage page, ReceiptFlowVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getReceiptNoticeNo()), KingdeeReceiptFlow::getReceiptNoticeNo, + query.getReceiptNoticeNo()) + .like(Func.isNotEmpty(query.getCounterpartyName()), KingdeeReceiptFlow::getCounterpartyName, + query.getCounterpartyName()) + .like(Func.isNotEmpty(query.getCounterpartyBank()), KingdeeReceiptFlow::getCounterpartyBank, + query.getCounterpartyBank()) + .like(Func.isNotEmpty(query.getCounterpartyAccount()), KingdeeReceiptFlow::getCounterpartyAccount, + query.getCounterpartyAccount()) + .like(Func.isNotEmpty(query.getSummary()), KingdeeReceiptFlow::getSummary, query.getSummary()) + .eq(Func.isNotEmpty(query.getClaimStatus()), KingdeeReceiptFlow::getClaimStatus, + query.getClaimStatus()) + .ge(query.getTransactionStartTime() != null, KingdeeReceiptFlow::getTransactionTime, + query.getTransactionStartTime()) + .le(query.getTransactionEndTime() != null, KingdeeReceiptFlow::getTransactionTime, + query.getTransactionEndTime()) + .eq(KingdeeReceiptFlow::getStatus, 1) + .orderByDesc(KingdeeReceiptFlow::getCreateTime); + return page(page, wrapper).convert(ReceiptFlowWrapper.build()::entityVO); + } + + @Override + public ReceiptFlowVO detail(Long id) { + ReceiptFlowVO vo = ReceiptFlowWrapper.build().entityVO(existing(id)); + vo.setClaimerName(UserCache.getUserRealName(AuthUtil.getUserId())); + Long deptId = Func.firstLong(AuthUtil.getDeptId()); + Dept dept = deptId == null ? null : SysCache.getDept(deptId); + vo.setClaimerDeptName(dept == null ? null : dept.getDeptName()); + vo.setClaimDate(LocalDate.now()); + return vo; + } + + @Override + public List> settlementCandidates(String keyword, Long flowId) { + KingdeeReceiptFlow flow = existing(flowId); + List settlements = formalSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlement::getSettlementType, RECEIVABLE) + .eq(FormalSettlement::getApprovalStatus, APPROVED) + .eq(FormalSettlement::getStatus, 1) + .and(Func.isNotEmpty(keyword), wrapper -> wrapper + .like(FormalSettlement::getFormalSettlementNo, keyword) + .or().like(FormalSettlement::getProjectName, keyword) + .or().like(FormalSettlement::getContractName, keyword)) + .orderByDesc(FormalSettlement::getCreateTime) + .last("limit 200")); + return settlements.stream() + .filter(settlement -> Func.isEmpty(flow.getCounterpartyName()) + || sameName(flow.getCounterpartyName(), settlement.getPayerName())) + .map(settlement -> candidateRow(settlement)) + .filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0) + .toList(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long claim(ReceiptClaimRequest request) { + if (request == null || request.getFlowId() == null) { + throw new ServiceException("收款流水ID不能为空"); + } + if (request.getSettlements() == null || request.getSettlements().isEmpty()) { + throw new ServiceException("请选择应收正式结算单"); + } + validateLength(request.getRemark(), 200, "备注不能超过200字"); + + KingdeeReceiptFlow flow = lockedFlow(request.getFlowId()); + Map allocationMap = allocationMap(request.getSettlements()); + List settlementIds = allocationMap.keySet().stream().sorted().toList(); + Map settlementMap = lockSettlements(settlementIds); + List settlements = settlementIds.stream().map(settlementMap::get).toList(); + assertCompatible(settlements); + assertCounterparty(flow, settlements.get(0)); + + Map previousClaimedMap = new LinkedHashMap<>(); + BigDecimal allocatedTotal = BigDecimal.ZERO; + for (FormalSettlement settlement : settlements) { + BigDecimal allocated = allocationMap.get(settlement.getId()); + BigDecimal previousClaimed = settlementClaimedAmount(settlement.getId()); + BigDecimal settlementAmount = positiveMoney(settlement.getSettlementAmount(), "结算总金额"); + if (previousClaimed.add(allocated).compareTo(settlementAmount) > 0) { + throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + + "的累计认领金额不能超过结算总应收含税金额"); + } + previousClaimedMap.put(settlement.getId(), previousClaimed); + allocatedTotal = allocatedTotal.add(allocated); + } + + BigDecimal receiptAmount = positiveMoney(flow.getReceiptAmount(), "收款金额"); + BigDecimal previousFlowClaimed = flowClaimedAmount(flow.getId()); + if (previousFlowClaimed.add(allocatedTotal).compareTo(receiptAmount) > 0) { + throw new ServiceException("本次分摊金额不能超过流水剩余可认领金额"); + } + + Dept dept = TransportBusinessSupport.currentDept("收款流水认领"); + ReceiptClaim claim = new ReceiptClaim(); + claim.setReceiptFlowId(flow.getId()); + claim.setClaimAmount(allocatedTotal); + claim.setClaimerId(AuthUtil.getUserId()); + claim.setClaimerName(UserCache.getUserRealName(AuthUtil.getUserId())); + claim.setClaimerDeptId(dept.getId()); + claim.setClaimerDeptName(dept.getDeptName()); + claim.setClaimDate(LocalDate.now()); + claim.setAttachmentsJson(request.getAttachmentsJson()); + claim.setRemark(trimToNull(request.getRemark())); + claim.setClaimStatus(CLAIMED); + claim.setKingdeeBillStatus("none"); + claimMapper.insert(claim); + + for (FormalSettlement settlement : settlements) { + BigDecimal previousClaimed = previousClaimedMap.get(settlement.getId()); + BigDecimal allocated = allocationMap.get(settlement.getId()); + BigDecimal claimedAfter = previousClaimed.add(allocated); + + ReceiptClaimSettlement relation = new ReceiptClaimSettlement(); + relation.setReceiptClaimId(claim.getId()); + relation.setReceiptFlowId(flow.getId()); + relation.setFormalSettlementId(settlement.getId()); + relation.setFormalSettlementNo(settlement.getFormalSettlementNo()); + relation.setSettlementAmount(money(settlement.getSettlementAmount())); + relation.setClaimedReceiptAmount(previousClaimed); + relation.setAllocatedReceiptAmount(allocated); + claimSettlementMapper.insert(relation); + + settlement.setPaidAmount(claimedAfter); + settlement.setPaymentStatus(amountStatus(claimedAfter, settlement.getSettlementAmount())); + formalSettlementMapper.updateById(settlement); + } + + String fromStatus = normalizeClaimStatus(flow.getClaimStatus()); + BigDecimal claimedAfter = previousFlowClaimed.add(allocatedTotal); + String toStatus = amountClaimStatus(claimedAfter, receiptAmount); + flow.setClaimedAmount(claimedAfter); + flow.setClaimStatus(toStatus); + updateById(flow); + record(flow.getId(), claim.getId(), "claim", "认领收款流水", fromStatus, toStatus, + allocatedTotal, "关联" + settlements.size() + "张应收正式结算单"); + return claim.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int sync(ReceiptFlowSyncRequest request) { + List rows = request == null || request.getFlows() == null + ? List.of() : request.getFlows(); + if (rows.isEmpty()) { + record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO, + "未接收到金蝶流水数据"); + return 0; + } + + Set serialNumbers = new HashSet<>(); + int syncedCount = 0; + for (ReceiptFlowSyncRequest.FlowRow row : rows) { + validateSyncRow(row); + if (!serialNumbers.add(row.getDetailSerialNo().trim())) { + throw new ServiceException("明细流水号" + row.getDetailSerialNo() + "重复"); + } + KingdeeReceiptFlow entity = baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(KingdeeReceiptFlow::getDetailSerialNo, row.getDetailSerialNo().trim()) + .last("FOR UPDATE")); + boolean created = entity == null; + if (created) { + entity = new KingdeeReceiptFlow(); + entity.setClaimedAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + entity.setClaimStatus(UNCLAIMED); + } + BigDecimal receiptAmount = positiveMoney(row.getReceiptAmount(), "收款金额"); + BigDecimal claimedAmount = money(entity.getClaimedAmount()); + if (claimedAmount.compareTo(receiptAmount) > 0) { + throw new ServiceException("流水" + row.getDetailSerialNo() + "同步金额不能小于已认领金额"); + } + copySyncRow(entity, row, receiptAmount); + entity.setClaimStatus(amountClaimStatus(claimedAmount, receiptAmount)); + if (created) { + baseMapper.insert(entity); + } else { + baseMapper.updateById(entity); + } + record(entity.getId(), null, "sync", created ? "新增金蝶收款流水" : "更新金蝶收款流水", + entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo()); + syncedCount++; + } + return syncedCount; + } + + private Map candidateRow(FormalSettlement settlement) { + BigDecimal claimedAmount = settlementClaimedAmount(settlement.getId()); + BigDecimal settlementAmount = money(settlement.getSettlementAmount()); + Map row = new LinkedHashMap<>(); + row.put("id", settlement.getId()); + row.put("formalSettlementNo", settlement.getFormalSettlementNo()); + row.put("projectId", settlement.getProjectId()); + row.put("projectName", settlement.getProjectName()); + row.put("deptId", settlement.getDeptId()); + row.put("deptName", settlement.getDeptName()); + row.put("contractId", settlement.getContractId()); + row.put("contractNo", settlement.getContractNo()); + row.put("contractName", settlement.getContractName()); + row.put("payerName", settlement.getPayerName()); + row.put("payeeName", settlement.getPayeeName()); + row.put("settlementAmount", settlementAmount); + row.put("claimedReceiptAmount", claimedAmount); + row.put("remainingReceiptAmount", settlementAmount.subtract(claimedAmount).max(BigDecimal.ZERO)); + return row; + } + + private Map allocationMap(List rows) { + Map allocationMap = new LinkedHashMap<>(); + for (ReceiptClaimRequest.SettlementRow row : rows) { + if (row == null || row.getSettlementId() == null) { + throw new ServiceException("结算单ID不能为空"); + } + if (allocationMap.containsKey(row.getSettlementId())) { + throw new ServiceException("同一张结算单不能重复分摊"); + } + allocationMap.put(row.getSettlementId(), positiveMoney(row.getAllocatedReceiptAmount(), + "分摊收款金额")); + } + return allocationMap; + } + + private Map lockSettlements(List settlementIds) { + List settlements = new ArrayList<>(); + for (Long settlementId : settlementIds) { + FormalSettlement settlement = formalSettlementMapper.selectOne( + Wrappers.lambdaQuery() + .eq(FormalSettlement::getId, settlementId) + .last("FOR UPDATE")); + settlements.add(availableSettlement(settlement)); + } + return settlements.stream().collect(Collectors.toMap(FormalSettlement::getId, + Function.identity(), (first, second) -> first, LinkedHashMap::new)); + } + + private FormalSettlement availableSettlement(FormalSettlement settlement) { + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1) + || !Objects.equals(settlement.getStatus(), 1) + || !APPROVED.equals(settlement.getApprovalStatus()) + || !RECEIVABLE.equals(settlement.getSettlementType())) { + throw new ServiceException("只能选择审批通过、未作废的应收正式结算单"); + } + return settlement; + } + + private void assertCompatible(List settlements) { + if (settlements.isEmpty()) { + throw new ServiceException("请选择应收正式结算单"); + } + FormalSettlement first = settlements.get(0); + if (settlements.stream().anyMatch(item -> !Objects.equals(first.getProjectId(), item.getProjectId()) + || !Objects.equals(first.getDeptId(), item.getDeptId()) + || !Objects.equals(first.getPayerName(), item.getPayerName()) + || !Objects.equals(first.getPayeeName(), item.getPayeeName()))) { + throw new ServiceException("关联结算单必须属于同一项目、组织及收付款方"); + } + } + + private void assertCounterparty(KingdeeReceiptFlow flow, FormalSettlement settlement) { + if (!sameName(flow.getCounterpartyName(), settlement.getPayerName())) { + throw new ServiceException("对方户名与结算单付款方不一致"); + } + } + + private boolean sameName(String first, String second) { + return Func.isNotEmpty(first) && Func.isNotEmpty(second) && first.trim().equals(second.trim()); + } + + private BigDecimal settlementClaimedAmount(Long settlementId) { + return claimSettlementMapper.selectList(Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getFormalSettlementId, settlementId) + .eq(ReceiptClaimSettlement::getStatus, 1)).stream() + .map(ReceiptClaimSettlement::getAllocatedReceiptAmount) + .map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal flowClaimedAmount(Long flowId) { + return claimSettlementMapper.selectList(Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getReceiptFlowId, flowId) + .eq(ReceiptClaimSettlement::getStatus, 1)).stream() + .map(ReceiptClaimSettlement::getAllocatedReceiptAmount) + .map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private KingdeeReceiptFlow lockedFlow(Long flowId) { + KingdeeReceiptFlow flow = baseMapper.selectOne(Wrappers.lambdaQuery() + .eq(KingdeeReceiptFlow::getId, flowId) + .last("FOR UPDATE")); + if (flow == null || Objects.equals(flow.getIsDeleted(), 1) || !Objects.equals(flow.getStatus(), 1)) { + throw new ServiceException("收款流水不存在或已失效"); + } + return flow; + } + + private KingdeeReceiptFlow existing(Long id) { + if (id == null) { + throw new ServiceException("收款流水ID不能为空"); + } + KingdeeReceiptFlow flow = getById(id); + if (flow == null || Objects.equals(flow.getIsDeleted(), 1) || !Objects.equals(flow.getStatus(), 1)) { + throw new ServiceException("收款流水不存在或已失效"); + } + return flow; + } + + private void validateSyncRow(ReceiptFlowSyncRequest.FlowRow row) { + if (row == null) { + throw new ServiceException("金蝶收款流水不能为空"); + } + required(row.getReceiptNoticeNo(), "认领通知单"); + required(row.getPayerName(), "付款人"); + required(row.getCounterpartyName(), "对方户名"); + required(row.getCounterpartyAccount(), "对方账号"); + required(row.getCounterpartyBank(), "对方开户行"); + required(row.getDetailSerialNo(), "明细流水号"); + if (row.getTransactionTime() == null) { + throw new ServiceException("交易时间不能为空"); + } + validateLength(row.getReceiptNoticeNo(), 100, "认领通知单不能超过100字"); + validateLength(row.getPayerName(), 200, "付款人不能超过200字"); + validateLength(row.getCounterpartyName(), 200, "对方户名不能超过200字"); + validateLength(row.getCounterpartyAccount(), 100, "对方账号不能超过100字"); + validateLength(row.getCounterpartyBank(), 200, "对方开户行不能超过200字"); + validateLength(row.getSummary(), 500, "摘要不能超过500字"); + validateLength(row.getDetailSerialNo(), 100, "明细流水号不能超过100字"); + positiveMoney(row.getReceiptAmount(), "收款金额"); + } + + private void copySyncRow(KingdeeReceiptFlow target, ReceiptFlowSyncRequest.FlowRow source, + BigDecimal receiptAmount) { + target.setReceiptNoticeNo(source.getReceiptNoticeNo().trim()); + target.setPayerName(source.getPayerName().trim()); + target.setReceiptAmount(receiptAmount); + target.setCounterpartyName(source.getCounterpartyName().trim()); + target.setCounterpartyAccount(source.getCounterpartyAccount().trim()); + target.setCounterpartyBank(source.getCounterpartyBank().trim()); + target.setSummary(trimToNull(source.getSummary())); + target.setTransactionTime(source.getTransactionTime()); + target.setDetailSerialNo(source.getDetailSerialNo().trim()); + target.setSourceUpdatedTime(source.getSourceUpdatedTime() == null + ? LocalDateTime.now() : source.getSourceUpdatedTime()); + } + + private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal receiptAmount) { + if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) { + return UNCLAIMED; + } + return claimedAmount.compareTo(money(receiptAmount)) >= 0 ? CLAIMED : PARTIAL; + } + + private String amountStatus(BigDecimal claimedAmount, BigDecimal settlementAmount) { + if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) { + return "unpaid"; + } + return claimedAmount.compareTo(money(settlementAmount)) >= 0 ? "paid" : "partial"; + } + + private String normalizeClaimStatus(String claimStatus) { + return List.of(UNCLAIMED, PARTIAL, CLAIMED).contains(claimStatus) ? claimStatus : UNCLAIMED; + } + + private BigDecimal positiveMoney(BigDecimal amount, String fieldName) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException(fieldName + "必须大于0"); + } + if (amount.stripTrailingZeros().scale() > 2) { + throw new ServiceException(fieldName + "最多保留2位小数"); + } + return amount.setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal money(BigDecimal amount) { + return amount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) + : amount.setScale(2, RoundingMode.HALF_UP); + } + + private String required(String value, String fieldName) { + String result = trimToNull(value); + if (result == null) { + throw new ServiceException(fieldName + "不能为空"); + } + return result; + } + + private String trimToNull(String value) { + return value == null || value.trim().isEmpty() ? null : value.trim(); + } + + private void validateLength(String value, int maxLength, String message) { + if (value != null && value.length() > maxLength) { + throw new ServiceException(message); + } + } + + private void record(Long flowId, Long claimId, String actionType, String actionName, + String fromStatus, String toStatus, BigDecimal operationAmount, String content) { + ReceiptFlowRecord record = new ReceiptFlowRecord(); + record.setReceiptFlowId(flowId); + record.setReceiptClaimId(claimId); + record.setActionType(actionType); + record.setActionName(actionName); + record.setFromStatus(fromStatus); + record.setToStatus(toStatus); + record.setOperationAmount(money(operationAmount)); + record.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName()); + record.setContent(content); + recordMapper.insert(record); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillLedgerWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillLedgerWrapper.java new file mode 100644 index 0000000..1dba52a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillLedgerWrapper.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.BillLedger; +import org.springblade.transport.pojo.vo.BillLedgerVO; + +import java.time.LocalDate; +import java.util.Objects; + +/** 汇票台账包装器。 @author Chill */ +public class BillLedgerWrapper extends BaseEntityWrapper { + public static BillLedgerWrapper build() { + return new BillLedgerWrapper(); + } + + @Override + public BillLedgerVO entityVO(BillLedger entity) { + BillLedgerVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, BillLedgerVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setBillTypeName(switch (entity.getBillType() == null ? "" : entity.getBillType()) { + case "issued" -> "开票"; + case "received" -> "收票"; + default -> entity.getBillType(); + }); + LocalDate today = LocalDate.now(); + if (entity.getMaturityDate() == null) { + vo.setMaturityStatusName(""); + } else if (entity.getMaturityDate().isBefore(today)) { + vo.setMaturityStatusName("已到期"); + } else if (entity.getMaturityDate().isEqual(today)) { + vo.setMaturityStatusName("今日到期"); + } else { + vo.setMaturityStatusName("未到期"); + } + return vo; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillPaymentWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillPaymentWrapper.java new file mode 100644 index 0000000..608e709 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/BillPaymentWrapper.java @@ -0,0 +1,57 @@ +/** + * 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.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.BillPayment; +import org.springblade.transport.pojo.vo.BillPaymentVO; + +import java.util.Objects; + +/** 汇票付款包装器。 @author Chill */ +public class BillPaymentWrapper extends BaseEntityWrapper { + public static BillPaymentWrapper build() { + return new BillPaymentWrapper(); + } + + @Override + public BillPaymentVO entityVO(BillPayment entity) { + BillPaymentVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, BillPaymentVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> entity.getApprovalStatus(); + }); + return vo; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceApplicationWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceApplicationWrapper.java new file mode 100644 index 0000000..5528198 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceApplicationWrapper.java @@ -0,0 +1,66 @@ +/** + * 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.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.InvoiceApplication; +import org.springblade.transport.pojo.vo.InvoiceApplicationVO; + +import java.util.Objects; + +/** + * 开票申请包装类 + * + * @author Chill + */ +public class InvoiceApplicationWrapper extends BaseEntityWrapper { + public static InvoiceApplicationWrapper build() { + return new InvoiceApplicationWrapper(); + } + + @Override + public InvoiceApplicationVO entityVO(InvoiceApplication entity) { + InvoiceApplicationVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, InvoiceApplicationVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> entity.getApprovalStatus(); + }); + vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) { + case "unsynced" -> "未同步"; + case "synced" -> "已同步"; + case "failed" -> "同步失败"; + default -> entity.getKingdeeStatus(); + }); + return vo; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceReceiptWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceReceiptWrapper.java new file mode 100644 index 0000000..e1039f0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/InvoiceReceiptWrapper.java @@ -0,0 +1,67 @@ +/** + * 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.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.InvoiceReceipt; +import org.springblade.transport.pojo.vo.InvoiceReceiptVO; + +import java.util.Objects; + +/** + * 收票登记包装器 + * + * @author Chill + */ +public class InvoiceReceiptWrapper extends BaseEntityWrapper { + + public static InvoiceReceiptWrapper build() { + return new InvoiceReceiptWrapper(); + } + + @Override + public InvoiceReceiptVO entityVO(InvoiceReceipt entity) { + InvoiceReceiptVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, InvoiceReceiptVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> entity.getApprovalStatus(); + }); + vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) { + case "synced" -> "已同步"; + case "failed" -> "同步失败"; + default -> "未同步"; + }); + return vo; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java new file mode 100644 index 0000000..7d4f482 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java @@ -0,0 +1,59 @@ +/** + * 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.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springblade.transport.pojo.vo.PaymentApplicationVO; + +import java.util.Objects; + +/** 付款申请包装器。 @author Chill */ +public class PaymentApplicationWrapper extends BaseEntityWrapper { + public static PaymentApplicationWrapper build() { return new PaymentApplicationWrapper(); } + @Override + public PaymentApplicationVO entityVO(PaymentApplication entity) { + PaymentApplicationVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, PaymentApplicationVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setPaymentTypeName(switch (entity.getPaymentType() == null ? "" : entity.getPaymentType()) { + case "project_advance" -> "项目预付"; + case "progress_advance" -> "进度预付"; + case "settlement_payment" -> "结算付款"; + default -> entity.getPaymentType(); + }); + vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> entity.getApprovalStatus(); + }); + vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) { + case "synced" -> "已生成"; + case "failed" -> "生成失败"; + default -> "未生成"; + }); + return vo; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java new file mode 100644 index 0000000..1aba22d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java @@ -0,0 +1,67 @@ +/** + * 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.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springblade.transport.pojo.vo.ReceiptFlowVO; + +import java.math.BigDecimal; +import java.util.Objects; + +/** + * 收款流水包装器 + * + * @author Chill + */ +public class ReceiptFlowWrapper extends BaseEntityWrapper { + + public static ReceiptFlowWrapper build() { + return new ReceiptFlowWrapper(); + } + + @Override + public ReceiptFlowVO entityVO(KingdeeReceiptFlow entity) { + ReceiptFlowVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, ReceiptFlowVO.class)); + vo.setClaimStatusName(switch (Objects.toString(entity.getClaimStatus(), "")) { + case "partial" -> "部分认领"; + case "claimed" -> "认领完成"; + default -> "未认领"; + }); + BigDecimal receiptAmount = money(entity.getReceiptAmount()); + BigDecimal claimedAmount = money(entity.getClaimedAmount()); + vo.setRemainingAmount(receiptAmount.subtract(claimedAmount).max(BigDecimal.ZERO)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + return vo; + } + + private BigDecimal money(BigDecimal amount) { + return amount == null ? BigDecimal.ZERO : amount; + } +} diff --git a/doc/sql/transport/blade_bill_ledger_20260821.sql b/doc/sql/transport/blade_bill_ledger_20260821.sql new file mode 100644 index 0000000..f3a8eb5 --- /dev/null +++ b/doc/sql/transport/blade_bill_ledger_20260821.sql @@ -0,0 +1,113 @@ +-- 首付款管理 / 汇票台账 +CREATE TABLE IF NOT EXISTS `blade_bill_ledger` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `bill_no` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '票据号码', + `issuer_id` bigint(20) NOT NULL COMMENT '出票单位客商ID', + `issuer_name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '出票单位', + `receiver_id` bigint(20) DEFAULT NULL COMMENT '收票单位ID', + `receiver_name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '收票单位', + `bill_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL COMMENT '汇票类型:issued开票、received收票', + `face_amount` decimal(18,2) NOT NULL COMMENT '票面金额', + `available_balance` decimal(18,2) NOT NULL COMMENT '可用余额', + `issue_date` date NOT NULL COMMENT '出票日期', + `maturity_date` date NOT NULL COMMENT '到期日期', + `available_dept_ids_json` longtext COLLATE utf8mb4_general_ci NOT NULL COMMENT '可用部门ID列表', + `available_dept_names` varchar(500) COLLATE utf8mb4_general_ci NOT NULL COMMENT '可用部门名称', + `fee_bearer_id` bigint(20) NOT NULL COMMENT '费用承担方客商ID', + `fee_bearer_name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用承担方', + `confirmed_discount_rate` decimal(8,4) DEFAULT NULL COMMENT '双方确认贴现率(%)', + `issuing_bank` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '出票行', + `bank_discount_reference_rate` decimal(8,4) DEFAULT NULL COMMENT '银行贴现参考率(%)', + `estimated_discount_fee` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '预计贴现费用', + `attachments_json` longtext COLLATE utf8mb4_general_ci COMMENT '附件JSON', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_bill_ledger_no` (`tenant_id`,`bill_no`), + KEY `idx_bill_ledger_maturity` (`tenant_id`,`maturity_date`), + KEY `idx_bill_ledger_parties` (`tenant_id`,`issuer_name`,`receiver_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='汇票台账'; + +CREATE TABLE IF NOT EXISTS `blade_bill_ledger_usage` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `bill_ledger_id` bigint(20) NOT NULL COMMENT '汇票台账ID', + `payment_application_id` bigint(20) NOT NULL COMMENT '付款申请ID', + `application_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '申请单号', + `used_amount` decimal(18,2) NOT NULL COMMENT '使用金额', + `use_dept_id` bigint(20) DEFAULT NULL COMMENT '使用部门ID', + `use_dept_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '使用部门', + `usage_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'approved' COMMENT '状态:approved已使用、released已释放', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_bill_ledger_usage_payment` (`tenant_id`,`payment_application_id`), + KEY `idx_bill_ledger_usage_ledger` (`tenant_id`,`bill_ledger_id`,`usage_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='汇票使用记录'; + +-- 以下结构升级支持重复执行,兼容前一次已执行 DDL、仅菜单插入失败的场景 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_payment_application` ADD COLUMN `bill_ledger_id` bigint(20) DEFAULT NULL COMMENT ''汇票台账ID'' AFTER `payment_method`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_payment_application' + AND COLUMN_NAME = 'bill_ledger_id' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_payment_application` ADD COLUMN `bill_no` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''票据号码'' AFTER `bill_ledger_id`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_payment_application' + AND COLUMN_NAME = 'bill_no' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_payment_application` ADD KEY `idx_payment_application_bill_ledger` (`bill_ledger_id`)', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_payment_application' + AND INDEX_NAME = 'idx_payment_application_bill_ledger' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +INSERT INTO `blade_menu` + (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +VALUES + (2090000000000001200,0,'first_payment_management','首付款管理','first_payment_management','/payment','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0), + (2090000000000001280,2090000000000001200,'bill_ledger','汇票台账','bill_ledger','/payment/bill-ledger','',6,1,0,1,NULL,'',0), + (2090000000000001281,2090000000000001280,'bill_ledger_view','查看','bill_ledger_view','','',1,2,0,1,NULL,'',0), + (2090000000000001282,2090000000000001280,'bill_ledger_add','新增','bill_ledger_add','','',2,2,0,1,NULL,'',0), + (2090000000000001283,2090000000000001280,'bill_ledger_edit','编辑','bill_ledger_edit','','',3,2,0,1,NULL,'',0), + (2090000000000001284,2090000000000001280,'bill_ledger_delete','删除','bill_ledger_delete','','',4,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE + `name`=VALUES(`name`), + `path`=VALUES(`path`), + `component`=VALUES(`component`), + `is_deleted`=0; diff --git a/doc/sql/transport/blade_bill_payment_20260822.sql b/doc/sql/transport/blade_bill_payment_20260822.sql new file mode 100644 index 0000000..f9be14a --- /dev/null +++ b/doc/sql/transport/blade_bill_payment_20260822.sql @@ -0,0 +1,79 @@ +-- 首付款管理 / 汇票付款 +CREATE TABLE IF NOT EXISTS `blade_bill_payment` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `payment_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '单据号', + `bill_ledger_id` bigint(20) NOT NULL COMMENT '汇票台账ID', + `bill_no` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '票据号码', + `face_amount` decimal(18,2) NOT NULL COMMENT '票面金额', + `available_balance` decimal(18,2) NOT NULL COMMENT '可用余额快照', + `used_amount` decimal(18,2) NOT NULL COMMENT '本次使用金额', + `dept_id` bigint(20) NOT NULL COMMENT '使用部门ID', + `dept_name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '使用部门', + `payment_date` date NOT NULL COMMENT '付款日期', + `approval_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'draft' COMMENT '单据状态', + `current_node` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前节点', + `current_processor` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前处理人', + `attachments_json` longtext COLLATE utf8mb4_general_ci COMMENT '附件JSON', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_bill_payment_no` (`tenant_id`,`payment_no`), + KEY `idx_bill_payment_ledger` (`tenant_id`,`bill_ledger_id`), + KEY `idx_bill_payment_date` (`tenant_id`,`payment_date`), + KEY `idx_bill_payment_status` (`tenant_id`,`approval_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='汇票付款'; + +-- 统一汇票台账使用记录:兼容付款申请与独立汇票付款单据 +ALTER TABLE `blade_bill_ledger_usage` + MODIFY COLUMN `payment_application_id` bigint(20) DEFAULT NULL COMMENT '付款申请ID'; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_bill_ledger_usage` ADD COLUMN `bill_payment_id` bigint(20) DEFAULT NULL COMMENT ''汇票付款ID'' AFTER `payment_application_id`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_bill_ledger_usage' + AND COLUMN_NAME = 'bill_payment_id' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_bill_ledger_usage` ADD UNIQUE KEY `uk_bill_ledger_usage_bill_payment` (`tenant_id`,`bill_payment_id`)', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_bill_ledger_usage' + AND INDEX_NAME = 'uk_bill_ledger_usage_bill_payment' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +INSERT INTO `blade_menu` + (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +VALUES + (2090000000000001200,0,'first_payment_management','首付款管理','first_payment_management','/payment','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0), + (2090000000000001290,2090000000000001200,'bill_payment','汇票付款','bill_payment','/payment/bill-payment','',7,1,0,1,NULL,'',0), + (2090000000000001291,2090000000000001290,'bill_payment_view','查看','bill_payment_view','','',1,2,0,1,NULL,'',0), + (2090000000000001292,2090000000000001290,'bill_payment_add','新增','bill_payment_add','','',2,2,0,1,NULL,'',0), + (2090000000000001293,2090000000000001290,'bill_payment_edit','编辑','bill_payment_edit','','',3,2,0,1,NULL,'',0), + (2090000000000001294,2090000000000001290,'bill_payment_delete','删除','bill_payment_delete','','',4,2,0,1,NULL,'',0), + (2090000000000001295,2090000000000001290,'bill_payment_submit','提交审批','bill_payment_submit','','',5,2,0,1,NULL,'',0), + (2090000000000001296,2090000000000001290,'bill_payment_approve','审批','bill_payment_approve','','',6,2,0,1,NULL,'',0), + (2090000000000001297,2090000000000001290,'bill_payment_void','作废','bill_payment_void','','',7,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE + `name`=VALUES(`name`), + `path`=VALUES(`path`), + `component`=VALUES(`component`), + `is_deleted`=0; diff --git a/doc/sql/transport/blade_invoice_application_20260821.sql b/doc/sql/transport/blade_invoice_application_20260821.sql new file mode 100644 index 0000000..67aa6a6 --- /dev/null +++ b/doc/sql/transport/blade_invoice_application_20260821.sql @@ -0,0 +1,187 @@ +-- 首付款管理 / 开票管理 +CREATE TABLE IF NOT EXISTS `blade_invoice_application` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `application_no` varchar(100) NOT NULL, + `project_id` bigint(20) DEFAULT NULL, + `project_name` varchar(100) DEFAULT NULL, + `dept_id` bigint(20) DEFAULT NULL, + `dept_name` varchar(100) DEFAULT NULL, + `issuer_name` varchar(200) DEFAULT NULL, + `receiver_customer_id` bigint(20) DEFAULT NULL, + `receiver_name` varchar(200) DEFAULT NULL, + `invoice_type` varchar(30) NOT NULL, + `available_invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `application_date` date DEFAULT NULL, + `applicant_name` varchar(100) DEFAULT NULL, + `undertaking_dept_id` bigint(20) DEFAULT NULL, + `undertaking_dept_name` varchar(100) DEFAULT NULL, + `department_emails` varchar(320) DEFAULT NULL, + `receiver_invoice_info_id` bigint(20) DEFAULT NULL, + `taxpayer_no` varchar(20) DEFAULT NULL, + `bank_name` varchar(100) DEFAULT NULL, + `bank_account` varchar(50) DEFAULT NULL, + `registered_address` varchar(200) DEFAULT NULL, + `contact_name` varchar(50) DEFAULT NULL, + `contact_phone` varchar(11) DEFAULT NULL, + `email` varchar(100) DEFAULT NULL, + `approval_status` varchar(30) NOT NULL DEFAULT 'draft', + `current_node` varchar(100) DEFAULT NULL, + `current_processor` varchar(200) DEFAULT NULL, + `kingdee_bill_no` varchar(100) DEFAULT NULL, + `kingdee_status` varchar(30) NOT NULL DEFAULT 'unsynced', + `synced_time` datetime DEFAULT NULL, + `attachments_json` longtext, + `remark` varchar(200) DEFAULT NULL, + `void_reason` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_invoice_application_no` (`tenant_id`,`application_no`), + KEY `idx_invoice_application_project` (`project_id`), + KEY `idx_invoice_application_status` (`approval_status`), + KEY `idx_invoice_application_kingdee` (`kingdee_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_application_settlement` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_application_id` bigint(20) NOT NULL, + `formal_settlement_id` bigint(20) NOT NULL, + `formal_settlement_no` varchar(100) NOT NULL, + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `available_invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `allocated_invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + PRIMARY KEY (`id`), + KEY `idx_invoice_application_settlement` (`invoice_application_id`,`formal_settlement_id`), + KEY `idx_invoice_settlement_formal` (`formal_settlement_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请关联结算单'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_application_sheet` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_application_id` bigint(20) NOT NULL, + `sheet_no` int(11) NOT NULL, + `invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + PRIMARY KEY (`id`), + KEY `idx_invoice_application_sheet` (`invoice_application_id`,`sheet_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请发票张次'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_application_line` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_application_id` bigint(20) NOT NULL, + `invoice_sheet_id` bigint(20) NOT NULL, + `line_no` int(11) NOT NULL, + `goods_category` varchar(100) NOT NULL, + `goods_name` varchar(100) NOT NULL, + `unit` varchar(30) DEFAULT NULL, + `quantity` decimal(18,4) DEFAULT NULL, + `unit_price_no_tax` decimal(18,4) DEFAULT NULL, + `amount_with_tax` decimal(18,2) NOT NULL DEFAULT '0.00', + `tax_rate` decimal(8,4) NOT NULL DEFAULT '0.0000', + `tax_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `remark` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_invoice_sheet_line` (`invoice_sheet_id`,`line_no`), + KEY `idx_invoice_line_application` (`invoice_application_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请商品行'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_application_detail` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_application_id` bigint(20) NOT NULL, + `formal_settlement_id` bigint(20) NOT NULL, + `formal_settlement_detail_id` bigint(20) NOT NULL, + `line_no` int(11) NOT NULL, + `document_no` varchar(100) DEFAULT NULL, + `waybill_no` varchar(100) DEFAULT NULL, + `vehicle_no` varchar(100) DEFAULT NULL, + `departure_address` varchar(500) DEFAULT NULL, + `arrival_address` varchar(500) DEFAULT NULL, + `actual_departure_time` datetime DEFAULT NULL, + `actual_completion_time` datetime DEFAULT NULL, + `transport_type` varchar(100) DEFAULT NULL, + `cargo_name` varchar(200) DEFAULT NULL, + `cargo_type` varchar(100) DEFAULT NULL, + `transport_quantity` decimal(18,4) DEFAULT NULL, + `quantity_unit` varchar(30) DEFAULT NULL, + `mileage` decimal(18,2) DEFAULT NULL, + `batch_no` varchar(100) DEFAULT NULL, + `freight_amount` decimal(18,2) DEFAULT NULL, + `fee_items_json` longtext, + `settlement_amount_tax` decimal(18,2) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_invoice_application_detail` (`invoice_application_id`,`formal_settlement_detail_id`), + KEY `idx_invoice_detail_settlement` (`formal_settlement_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请结算明细快照'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_application_record` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_application_id` bigint(20) NOT NULL, + `action_type` varchar(30) NOT NULL, + `action_name` varchar(50) NOT NULL, + `from_status` varchar(30) DEFAULT NULL, + `to_status` varchar(30) DEFAULT NULL, + `operator_name` varchar(100) DEFAULT NULL, + `reason` varchar(200) DEFAULT NULL, + `kingdee_bill_no` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_invoice_record_application` (`invoice_application_id`,`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请操作记录'; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000001200,0,'first_payment_management','首付款管理','first_payment_management','/payment','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0), +(2090000000000001220,2090000000000001200,'invoice_application','开票管理','invoice_application','/payment/invoice-application','',2,1,0,1,NULL,'',0), +(2090000000000001221,2090000000000001220,'invoice_application_view','查看','invoice_application_view','','',1,2,0,1,NULL,'',0), +(2090000000000001222,2090000000000001220,'invoice_application_add','新增','invoice_application_add','','',2,2,0,1,NULL,'',0), +(2090000000000001223,2090000000000001220,'invoice_application_edit','编辑','invoice_application_edit','','',3,2,0,1,NULL,'',0), +(2090000000000001224,2090000000000001220,'invoice_application_delete','删除','invoice_application_delete','','',4,2,0,1,NULL,'',0), +(2090000000000001225,2090000000000001220,'invoice_application_submit','提交审批','invoice_application_submit','','',5,2,0,1,NULL,'',0), +(2090000000000001226,2090000000000001220,'invoice_application_approve','审批','invoice_application_approve','','',6,2,0,1,NULL,'',0), +(2090000000000001227,2090000000000001220,'invoice_application_sync','同步金蝶','invoice_application_sync','','',7,2,0,1,NULL,'',0), +(2090000000000001228,2090000000000001220,'invoice_application_export','导出','invoice_application_export','','',8,2,0,1,NULL,'',0), +(2090000000000001229,2090000000000001220,'invoice_application_void','作废','invoice_application_void','','',9,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`component`=VALUES(`component`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_invoice_receipt_20260821.sql b/doc/sql/transport/blade_invoice_receipt_20260821.sql new file mode 100644 index 0000000..d00db21 --- /dev/null +++ b/doc/sql/transport/blade_invoice_receipt_20260821.sql @@ -0,0 +1,145 @@ +-- 首付款管理 / 收票管理 +CREATE TABLE IF NOT EXISTS `blade_kingdee_invoice_pool` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_no` varchar(32) COLLATE utf8mb4_general_ci NOT NULL, + `invoice_date` date DEFAULT NULL, + `invoice_type` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL, + `tax_rate` decimal(8,4) NOT NULL DEFAULT '0.0000', + `invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `tax_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `receiver_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `issuer_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `bank_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `bank_account` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `issuing_bank` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `phone` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL, + `customer_emails` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `department_emails` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `kingdee_bill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `kingdee_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'unsynced', + `attachments_json` longtext COLLATE utf8mb4_general_ci, + `source_updated_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_kingdee_invoice_pool_no` (`tenant_id`,`invoice_no`), + KEY `idx_kingdee_invoice_pool_date` (`invoice_date`), + KEY `idx_kingdee_invoice_pool_status` (`kingdee_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='金蝶进项发票票据池镜像'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_receipt` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `kingdee_invoice_pool_id` bigint(20) NOT NULL, + `invoice_no` varchar(32) COLLATE utf8mb4_general_ci NOT NULL, + `invoice_date` date DEFAULT NULL, + `invoice_type` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL, + `tax_rate` decimal(8,4) NOT NULL DEFAULT '0.0000', + `invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `tax_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `receiver_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `issuer_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `project_id` bigint(20) DEFAULT NULL, + `project_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `dept_id` bigint(20) DEFAULT NULL, + `dept_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `payer_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `bank_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `bank_account` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `issuing_bank` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `phone` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL, + `customer_emails` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `department_emails` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `approval_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'draft', + `current_node` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `current_processor` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `kingdee_bill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `kingdee_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'unsynced', + `attachments_json` longtext COLLATE utf8mb4_general_ci, + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `void_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_invoice_receipt_pool` (`kingdee_invoice_pool_id`), + KEY `idx_invoice_receipt_no` (`tenant_id`,`invoice_no`), + KEY `idx_invoice_receipt_project` (`project_id`), + KEY `idx_invoice_receipt_date` (`invoice_date`), + KEY `idx_invoice_receipt_status` (`approval_status`), + KEY `idx_invoice_receipt_kingdee` (`kingdee_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='收票登记'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_receipt_settlement` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_receipt_id` bigint(20) NOT NULL, + `formal_settlement_id` bigint(20) NOT NULL, + `formal_settlement_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL, + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `received_invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `allocated_invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + PRIMARY KEY (`id`), + KEY `idx_invoice_receipt_settlement` (`invoice_receipt_id`,`formal_settlement_id`), + KEY `idx_invoice_receipt_formal` (`formal_settlement_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='收票登记结算单分摊'; + +CREATE TABLE IF NOT EXISTS `blade_invoice_receipt_record` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `invoice_receipt_id` bigint(20) NOT NULL, + `action_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL, + `action_name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL, + `from_status` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL, + `to_status` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL, + `operator_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + `kingdee_bill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_invoice_receipt_record` (`invoice_receipt_id`,`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='收票登记操作记录'; + +INSERT INTO `blade_menu` + (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +VALUES + (2090000000000001200,0,'first_payment_management','首付款管理','first_payment_management','/payment','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0), + (2090000000000001240,2090000000000001200,'invoice_receipt','收票管理','invoice_receipt','/payment/invoice-receipt','',3,1,0,1,NULL,'',0), + (2090000000000001241,2090000000000001240,'invoice_receipt_view','查看','invoice_receipt_view','','',1,2,0,1,NULL,'',0), + (2090000000000001242,2090000000000001240,'invoice_receipt_add','新增','invoice_receipt_add','','',2,2,0,1,NULL,'',0), + (2090000000000001243,2090000000000001240,'invoice_receipt_edit','编辑','invoice_receipt_edit','','',3,2,0,1,NULL,'',0), + (2090000000000001244,2090000000000001240,'invoice_receipt_delete','删除','invoice_receipt_delete','','',4,2,0,1,NULL,'',0), + (2090000000000001245,2090000000000001240,'invoice_receipt_submit','提交审批','invoice_receipt_submit','','',5,2,0,1,NULL,'',0), + (2090000000000001246,2090000000000001240,'invoice_receipt_approve','审批','invoice_receipt_approve','','',6,2,0,1,NULL,'',0), + (2090000000000001247,2090000000000001240,'invoice_receipt_sync','同步金蝶','invoice_receipt_sync','','',7,2,0,1,NULL,'',0), + (2090000000000001248,2090000000000001240,'invoice_receipt_export','导出','invoice_receipt_export','','',8,2,0,1,NULL,'',0), + (2090000000000001249,2090000000000001240,'invoice_receipt_void','作废','invoice_receipt_void','','',9,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE + `name`=VALUES(`name`), + `path`=VALUES(`path`), + `component`=VALUES(`component`), + `is_deleted`=0; diff --git a/doc/sql/transport/blade_payment_application_20260821.sql b/doc/sql/transport/blade_payment_application_20260821.sql new file mode 100644 index 0000000..484c418 --- /dev/null +++ b/doc/sql/transport/blade_payment_application_20260821.sql @@ -0,0 +1,46 @@ +-- 首付款管理 / 付款管理 +CREATE TABLE IF NOT EXISTS `blade_payment_application` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `payment_no` varchar(100) NOT NULL, `payment_type` varchar(30) NOT NULL, `settlement_id` bigint(20) DEFAULT NULL, `settlement_no` varchar(100) DEFAULT NULL, + `pre_settlement_id` bigint(20) DEFAULT NULL, `pre_settlement_no` varchar(100) DEFAULT NULL, `project_id` bigint(20) DEFAULT NULL, `project_name` varchar(100) DEFAULT NULL, + `dept_id` bigint(20) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `contract_id` bigint(20) DEFAULT NULL, `contract_no` varchar(100) DEFAULT NULL, + `contract_name` varchar(100) DEFAULT NULL, `payer_name` varchar(200) DEFAULT NULL, `payee_name` varchar(200) DEFAULT NULL, + `settlement_amount` decimal(18,2) DEFAULT NULL, `payable_amount` decimal(18,2) DEFAULT NULL, `bill_type` varchar(30) DEFAULT NULL, + `payment_ratio` decimal(8,2) DEFAULT NULL, `applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `payment_method` varchar(30) NOT NULL, + `receipt_account_id` bigint(20) DEFAULT NULL, `receipt_account_name` varchar(200) DEFAULT NULL, `bank_name` varchar(200) DEFAULT NULL, `bank_account` varchar(100) DEFAULT NULL, + `applicant_name` varchar(100) DEFAULT NULL, `apply_date` date DEFAULT NULL, `invoice_status` varchar(30) DEFAULT 'unmatched', `matched_invoice_amount` decimal(18,2) DEFAULT '0.00', `paid_amount` decimal(18,2) DEFAULT '0.00', + `approval_status` varchar(30) NOT NULL DEFAULT 'draft', `current_node` varchar(100) DEFAULT NULL, `current_processor` varchar(200) DEFAULT NULL, + `kingdee_bill_no` varchar(100) DEFAULT NULL, `kingdee_status` varchar(30) NOT NULL DEFAULT 'unsynced', `attachments_json` longtext, `remark` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), UNIQUE KEY `uk_payment_application_no` (`tenant_id`,`payment_no`), KEY `idx_payment_application_settlement` (`settlement_id`), KEY `idx_payment_application_apply_date` (`apply_date`), KEY `idx_payment_application_status` (`approval_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='付款申请'; + +CREATE TABLE IF NOT EXISTS `blade_payment_application_invoice` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `payment_application_id` bigint(20) NOT NULL, `line_no` int(11) NOT NULL, `settlement_no` varchar(100) DEFAULT NULL, `invoice_no` varchar(100) DEFAULT NULL, `invoice_date` date DEFAULT NULL, + `invoice_type` varchar(30) DEFAULT NULL, `tax_rate` decimal(8,4) DEFAULT NULL, `invoice_amount` decimal(18,2) DEFAULT NULL, `matched_amount` decimal(18,2) DEFAULT NULL, `attachment_json` longtext, + PRIMARY KEY (`id`), KEY `idx_payment_invoice_application` (`payment_application_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='付款申请发票明细'; + +CREATE TABLE IF NOT EXISTS `blade_payment_application_record` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `payment_application_id` bigint(20) NOT NULL, `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `paid_date` date DEFAULT NULL, `payment_no` varchar(100) DEFAULT NULL, `voucher_json` longtext, `kingdee_bill_no` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), KEY `idx_payment_record_application` (`payment_application_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='付款申请付款记录'; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000001200,0,'first_payment_management','首付款管理','first_payment_management','/payment','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0), +(2090000000000001201,2090000000000001200,'payment_application','付款管理','payment_application','/payment/payment-application','',1,1,0,1,NULL,'',0), +(2090000000000001202,2090000000000001201,'payment_application_view','查看','payment_application_view','','',1,2,0,1,NULL,'',0), +(2090000000000001203,2090000000000001201,'payment_application_add','新增','payment_application_add','','',2,2,0,1,NULL,'',0), +(2090000000000001204,2090000000000001201,'payment_application_edit','编辑','payment_application_edit','','',3,2,0,1,NULL,'',0), +(2090000000000001205,2090000000000001201,'payment_application_delete','删除','payment_application_delete','','',4,2,0,1,NULL,'',0), +(2090000000000001206,2090000000000001201,'payment_application_submit','提交审批','payment_application_submit','','',5,2,0,1,NULL,'',0), +(2090000000000001207,2090000000000001201,'payment_application_approve','审批','payment_application_approve','','',6,2,0,1,NULL,'',0), +(2090000000000001208,2090000000000001201,'payment_application_sync','同步金蝶','payment_application_sync','','',7,2,0,1,NULL,'',0), +(2090000000000001209,2090000000000001201,'payment_application_export','导出','payment_application_export','','',8,2,0,1,NULL,'',0), +(2090000000000001210,2090000000000001201,'payment_application_void','作废','payment_application_void','','',9,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`component`=VALUES(`component`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_receipt_flow_20260821.sql b/doc/sql/transport/blade_receipt_flow_20260821.sql new file mode 100644 index 0000000..862d0c7 --- /dev/null +++ b/doc/sql/transport/blade_receipt_flow_20260821.sql @@ -0,0 +1,114 @@ +-- 首付款管理 / 收款流水 +CREATE TABLE IF NOT EXISTS `blade_kingdee_receipt_flow` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `receipt_notice_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL, + `payer_name` varchar(200) COLLATE utf8mb4_general_ci NOT NULL, + `receipt_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `counterparty_name` varchar(200) COLLATE utf8mb4_general_ci NOT NULL, + `counterparty_account` varchar(100) COLLATE utf8mb4_general_ci NOT NULL, + `counterparty_bank` varchar(200) COLLATE utf8mb4_general_ci NOT NULL, + `summary` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL, + `transaction_time` datetime NOT NULL, + `detail_serial_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL, + `claimed_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `claim_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'unclaimed', + `source_updated_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_receipt_flow_serial` (`tenant_id`,`detail_serial_no`), + KEY `idx_receipt_flow_notice` (`tenant_id`,`receipt_notice_no`), + KEY `idx_receipt_flow_transaction` (`tenant_id`,`transaction_time`), + KEY `idx_receipt_flow_status` (`tenant_id`,`claim_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='金蝶收款流水镜像'; + +CREATE TABLE IF NOT EXISTS `blade_receipt_claim` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `receipt_flow_id` bigint(20) NOT NULL, + `claim_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `claimer_id` bigint(20) DEFAULT NULL, + `claimer_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `claimer_dept_id` bigint(20) DEFAULT NULL, + `claimer_dept_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `claim_date` date DEFAULT NULL, + `attachments_json` longtext COLLATE utf8mb4_general_ci, + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_receipt_claim_flow` (`receipt_flow_id`), + KEY `idx_receipt_claim_date` (`tenant_id`,`claim_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='收款流水认领主单'; + +CREATE TABLE IF NOT EXISTS `blade_receipt_claim_settlement` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `receipt_claim_id` bigint(20) NOT NULL, + `receipt_flow_id` bigint(20) NOT NULL, + `formal_settlement_id` bigint(20) NOT NULL, + `formal_settlement_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL, + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `claimed_receipt_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `allocated_receipt_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + PRIMARY KEY (`id`), + KEY `idx_receipt_claim_settlement` (`receipt_claim_id`,`formal_settlement_id`), + KEY `idx_receipt_claim_formal` (`formal_settlement_id`,`status`), + KEY `idx_receipt_claim_flow_settlement` (`receipt_flow_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='收款认领结算单分摊'; + +CREATE TABLE IF NOT EXISTS `blade_receipt_flow_record` ( + `id` bigint(20) NOT NULL, + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000', + `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` int(11) DEFAULT '1', + `is_deleted` int(11) DEFAULT '0', + `receipt_flow_id` bigint(20) DEFAULT NULL, + `receipt_claim_id` bigint(20) DEFAULT NULL, + `action_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL, + `action_name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL, + `from_status` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL, + `to_status` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL, + `operation_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `operator_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `content` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_receipt_flow_record_flow` (`receipt_flow_id`,`create_time`), + KEY `idx_receipt_flow_record_claim` (`receipt_claim_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='收款流水同步及认领操作记录'; + +INSERT INTO `blade_menu` + (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +VALUES + (2090000000000001200,0,'first_payment_management','首付款管理','first_payment_management','/payment','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0), + (2090000000000001260,2090000000000001200,'receipt_flow','收款流水','receipt_flow','/payment/receipt-flow','',4,1,0,1,NULL,'',0), + (2090000000000001261,2090000000000001260,'receipt_flow_view','查看','receipt_flow_view','','',1,2,0,1,NULL,'',0), + (2090000000000001262,2090000000000001260,'receipt_flow_claim','认领','receipt_flow_claim','','',2,2,0,1,NULL,'',0), + (2090000000000001263,2090000000000001260,'receipt_flow_sync','手动同步流水','receipt_flow_sync','','',3,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE + `name`=VALUES(`name`), + `path`=VALUES(`path`), + `component`=VALUES(`component`), + `is_deleted`=0; diff --git a/doc/sql/transport/blade_receipt_flow_claim_record_20260821.sql b/doc/sql/transport/blade_receipt_flow_claim_record_20260821.sql new file mode 100644 index 0000000..8ed83d7 --- /dev/null +++ b/doc/sql/transport/blade_receipt_flow_claim_record_20260821.sql @@ -0,0 +1,107 @@ +-- 首付款管理 / 认领记录 +-- 以下结构升级支持重复执行,兼容前一次已执行 DDL、仅菜单插入失败的场景 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD COLUMN `claim_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''claimed'' COMMENT ''认领状态:claimed已认领、voided已作废'' AFTER `remark`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'claim_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD COLUMN `kingdee_bill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶认领冲单号'' AFTER `claim_status`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'kingdee_bill_no' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD COLUMN `kingdee_bill_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''none'' COMMENT ''金蝶单据状态'' AFTER `kingdee_bill_no`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'kingdee_bill_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD COLUMN `voided_by` bigint(20) DEFAULT NULL COMMENT ''作废人'' AFTER `kingdee_bill_status`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'voided_by' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD COLUMN `voided_by_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''作废人姓名'' AFTER `voided_by`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'voided_by_name' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD COLUMN `voided_time` datetime DEFAULT NULL COMMENT ''作废时间'' AFTER `voided_by_name`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'voided_time' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD KEY `idx_receipt_claim_owner_status` (`tenant_id`,`claimer_id`,`claim_status`,`claim_date`)', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND INDEX_NAME = 'idx_receipt_claim_owner_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_receipt_claim` ADD KEY `idx_receipt_claim_kingdee_status` (`tenant_id`,`kingdee_bill_status`)', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND INDEX_NAME = 'idx_receipt_claim_kingdee_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +UPDATE `blade_receipt_claim` +SET `claim_status` = 'claimed', + `kingdee_bill_status` = 'none' +WHERE `claim_status` IS NULL OR `claim_status` = ''; + +INSERT INTO `blade_menu` + (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +VALUES + (2090000000000001200,0,'first_payment_management','首付款管理','first_payment_management','/payment','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0), + (2090000000000001270,2090000000000001200,'receipt_claim_record','认领记录','receipt_claim_record','/payment/receipt-claim-record','',5,1,0,1,NULL,'',0), + (2090000000000001271,2090000000000001270,'receipt_claim_record_view','查看','receipt_claim_record_view','','',1,2,0,1,NULL,'',0), + (2090000000000001272,2090000000000001270,'receipt_claim_record_void','作废','receipt_claim_record_void','','',2,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE + `name`=VALUES(`name`), + `path`=VALUES(`path`), + `component`=VALUES(`component`), + `is_deleted`=0; From 79f4c35235d4b95f8e646f46e7a16ee0c9c2fb2b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 22 Aug 2026 21:11:12 +0800 Subject: [PATCH 036/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=85=8D=E8=BD=BD?= =?UTF-8?q?=E3=80=81=E6=80=BB=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/vo/BusinessRemoveResultVO.java | 3 + .../controller/LoadingManageController.java | 7 +- .../service/ILoadingManageService.java | 2 +- .../impl/LoadingManageServiceImpl.java | 93 ++++--------------- .../service/impl/MasterOrderServiceImpl.java | 7 +- 5 files changed, 28 insertions(+), 84 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BusinessRemoveResultVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BusinessRemoveResultVO.java index 9e1816d..ac8f537 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BusinessRemoveResultVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/BusinessRemoveResultVO.java @@ -52,4 +52,7 @@ public class BusinessRemoveResultVO implements Serializable { @Schema(description = "跳过编号") private List skippedCodes = new ArrayList<>(); + @Schema(description = "跳过原因") + private List skippedReasons = new ArrayList<>(); + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java index 39b5263..b1d29f9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java @@ -56,10 +56,9 @@ public class LoadingManageController extends BladeController { @GetMapping("/carrier-contracts") @ApiOperationSupport(order = 13) - @Operation(summary = "可选承运商合同", description = "根据运单ID集合查询共同可用的承运商合同") - public R> carrierContracts( - @Parameter(description = "运单ID集合", required = true) @RequestParam List waybillIds) { - return R.data(loadingManageService.carrierContracts(waybillIds)); + @Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同") + public R> carrierContracts() { + return R.data(loadingManageService.carrierContracts()); } @GetMapping("/list") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index dbb4539..73f4123 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -25,7 +25,7 @@ public interface ILoadingManageService extends BaseService { LoadingManageVO detail(Long id); - List carrierContracts(List waybillIds); + List carrierContracts(); boolean saveDraft(LoadingManage loadingManage); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 28cf212..a6b2113 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.jackson.JsonUtil; @@ -39,10 +40,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; /** * 配载管理 服务实现类 @@ -50,6 +48,7 @@ import java.util.stream.Collectors; * @author Chill */ @Service +@Slf4j public class LoadingManageServiceImpl extends BaseServiceImpl implements ILoadingManageService { private static final String STATUS_DRAFT = "draft"; @@ -86,8 +85,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl carrierContracts(List waybillIds) { - return findCarrierContracts(waybillIds).stream().map(contract -> { + public List carrierContracts() { + return availableCarrierContracts().stream().map(contract -> { LoadingCarrierContractVO option = new LoadingCarrierContractVO(); option.setId(contract.getId()); option.setContractName(contract.getContractName()); @@ -292,9 +291,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); - if (Func.isEmpty(waybillIdList)) { - return false; - } - List waybillList = waybillMapper.selectList(Wrappers.lambdaQuery() - .eq(Waybill::getIsDeleted, 0) - .in(Waybill::getId, waybillIdList)); - if (waybillList.size() != waybillIdList.size()) { - return false; - } - return waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_RUNNING)) - || waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_COMPLETED)); - } - private LambdaQueryWrapper buildQuery(LoadingManageVO loadingManage) { TransportBusinessSupport.validateAllDept(loadingManage.getAllDept(), "配载管理"); LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery().eq(LoadingManage::getIsDeleted, 0); @@ -604,11 +591,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl Objects.equals(contract.getId(), loadingManage.getCarrierContractId())) .findFirst() - .orElseThrow(() -> new ServiceException("所选承运商合同与运单绑定的客户合同不匹配或已失效")); + .orElseThrow(() -> new ServiceException("所选承运商合同不存在、未审核通过或已失效")); loadingManage.setCarrierName(carrierContract.getPartyB()); } @@ -623,58 +609,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl findCarrierContracts(List waybillIds) { - if (Func.isEmpty(waybillIds)) { - return new ArrayList<>(); - } - List distinctWaybillIds = waybillIds.stream() - .filter(Objects::nonNull) - .distinct() - .toList(); - if (Func.isEmpty(distinctWaybillIds)) { - return new ArrayList<>(); - } - List waybillList = waybillMapper.selectList(Wrappers.lambdaQuery() - .eq(Waybill::getIsDeleted, 0) - .in(Waybill::getId, distinctWaybillIds)); - if (waybillList.size() != distinctWaybillIds.size()) { - throw new ServiceException("待配载运单不存在或已删除"); - } - waybillList.forEach(waybill -> - TransportBusinessSupport.assertCurrentDept(waybill.getDeptId(), "运单管理")); - if (waybillList.stream().anyMatch(waybill -> Func.isEmpty(waybill.getContractId()))) { - throw new ServiceException("所选运单存在未绑定客户合同的数据"); - } - List customerContractIds = waybillList.stream() - .map(Waybill::getContractId) - .distinct() - .toList(); - Map customerContractMap = contractManageService.listByIds(customerContractIds) - .stream() - .filter(contract -> Objects.equals(contract.getIsDeleted(), 0)) - .collect(Collectors.toMap(ContractManage::getId, Function.identity())); - if (customerContractMap.size() != customerContractIds.size()) { - throw new ServiceException("所选运单绑定的客户合同不存在或已删除"); - } - Waybill firstWaybill = waybillList.get(0); - ContractManage firstCustomerContract = customerContractMap.get(firstWaybill.getContractId()); - if (!Objects.equals(firstCustomerContract.getContractCategory(), "客户合同") - || Func.isEmpty(firstCustomerContract.getPartyA())) { - throw new ServiceException("所选运单绑定的客户合同信息不完整"); - } - boolean relationMismatch = waybillList.stream().anyMatch(waybill -> { - ContractManage customerContract = customerContractMap.get(waybill.getContractId()); - return !Objects.equals(customerContract.getContractCategory(), "客户合同") - || !Objects.equals(waybill.getProjectId(), firstWaybill.getProjectId()) - || !Objects.equals(customerContract.getPartyA(), firstCustomerContract.getPartyA()); - }); - if (relationMismatch) { - throw new ServiceException("所选运单的客户合同不属于同一项目和甲方,无法共用承运商合同"); - } + private List availableCarrierContracts() { return contractManageService.list(Wrappers.lambdaQuery() .eq(ContractManage::getIsDeleted, 0) - .eq(ContractManage::getProjectId, firstWaybill.getProjectId()) - .eq(ContractManage::getPartyA, firstCustomerContract.getPartyA()) + .eq(ContractManage::getStatus, 1) .eq(ContractManage::getContractCategory, "承运商合同") .in(ContractManage::getApprovalStatus, "approved", "change_approved") .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) @@ -697,6 +635,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpllambdaUpdate() .in(Waybill::getId, waybillIds) .set(Waybill::getLoadingNo, null) + .set(Waybill::getCarrierContractId, null) + .set(Waybill::getCarrierName, null) .set(Waybill::getBusinessStatus, status)); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index 38cdfa5..782c24b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -428,18 +428,19 @@ public class MasterOrderServiceImpl extends BaseServiceImpl Date: Mon, 24 Aug 2026 00:58:54 +0800 Subject: [PATCH 037/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A2=84=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E3=80=81=E6=AD=A3=E5=BC=8F=E7=BB=93=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/dto/FormalSettlementSaveRequest.java | 12 + .../transport/pojo/entity/CommonRoute.java | 18 ++ .../entity/FormalSettlementSummaryFee.java | 40 +++ .../transport/pojo/vo/FormalSettlementVO.java | 2 + .../FormalSettlementController.java | 47 ++- .../FormalSettlementSummaryFeeMapper.java | 20 ++ .../service/IFormalSettlementService.java | 1 + .../service/IPreSettlementService.java | 9 +- .../service/impl/CommonRouteServiceImpl.java | 32 ++ .../impl/FormalSettlementServiceImpl.java | 273 +++++++++++++++++- .../impl/PreSettlementServiceImpl.java | 63 +++- ..._common_route_region_id_patch_20260823.sql | 7 + .../blade_formal_settlement_20260818.sql | 16 + ...formal_settlement_summary_fee_20260823.sql | 24 ++ doc/sql/transport/blade_tms_business.sql | 6 + 15 files changed, 530 insertions(+), 40 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java create mode 100644 doc/sql/transport/blade_common_route_region_id_patch_20260823.sql create mode 100644 doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java index 5bb3c82..6eb8f18 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -28,4 +28,16 @@ public class FormalSettlementSaveRequest implements Serializable { private BigDecimal exchangeRate; private String attachmentsJson; private String remark; + private List summaryFees; + + @Data + public static class SummaryFee implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String feeType; + private String feeItem; + private BigDecimal adjustAmount; + private String remark; + private Integer manualFlag; + } } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java index 54f4992..4671cb9 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java @@ -57,6 +57,15 @@ public class CommonRoute extends TenantEntity { @Schema(description = "发货地") private String departureName; + @Schema(description = "发货省ID") + private String departureProvinceId; + + @Schema(description = "发货市ID") + private String departureCityId; + + @Schema(description = "发货区ID") + private String departureDistrictId; + @Schema(description = "发货地址") private String departureAddress; @@ -78,6 +87,15 @@ public class CommonRoute extends TenantEntity { @Schema(description = "收货地") private String arrivalName; + @Schema(description = "收货省ID") + private String arrivalProvinceId; + + @Schema(description = "收货市ID") + private String arrivalCityId; + + @Schema(description = "收货区ID") + private String arrivalDistrictId; + @Schema(description = "收货地址") private String arrivalAddress; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java new file mode 100644 index 0000000..9292f68 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java @@ -0,0 +1,40 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算合计费用实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_summary_fee") +@Schema(description = "正式结算合计费用") +public class FormalSettlementSummaryFee extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private Integer lineNo; + private String feeType; + private String feeItem; + private BigDecimal originalAmount; + private BigDecimal adjustAmount; + private BigDecimal settlementAmount; + private String remark; + private Integer manualFlag; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java index 8dcd0fd..ba68b38 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -15,6 +15,7 @@ import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serial; @@ -39,5 +40,6 @@ public class FormalSettlementVO extends FormalSettlement { @TableField(exist = false) private String preSettlementNo; @TableField(exist = false) private List sources; @TableField(exist = false) private List details; + @TableField(exist = false) private List summaryFees; @TableField(exist = false) private List payments; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index 85b1a0f..b40f671 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -73,64 +73,79 @@ public class FormalSettlementController extends BladeController { @GetMapping("/contract-options") @ApiOperationSupport(order = 4) @Operation(summary = "可选合同") - public R>> contractOptions(@RequestParam(required = false) String keyword) { - return R.data(preSettlementService.contractOptions(keyword)); + public R>> contractOptions(@RequestParam(required = false) String keyword, + @RequestParam(required = false) Long projectId) { + return R.data(preSettlementService.contractOptions(keyword, projectId)); + } + + @GetMapping("/next-no") + @ApiOperationSupport(order = 5) + @Operation(summary = "获取最新正式结算单号") + public R nextNo() { + return R.data(formalSettlementService.nextNo()); + } + + @GetMapping("/fee-options") + @ApiOperationSupport(order = 6) + @Operation(summary = "可选费用类型及费用项") + public R>> feeOptions() { + return R.data(preSettlementService.feeOptions()); } @GetMapping("/candidate-details") - @ApiOperationSupport(order = 5) + @ApiOperationSupport(order = 7) @Operation(summary = "可选应收应付明细") public R>> candidateDetails(Query query, @RequestParam Long contractId, @RequestParam String settlementType, @RequestParam(required = false) String batchNo, - @RequestParam(required = false) String feeStartDate, @RequestParam(required = false) String feeEndDate) { - return R.data(preSettlementService.candidateDetails(Condition.getPage(query), contractId, - settlementType, batchNo, feeStartDate, feeEndDate)); + @RequestParam(required = false) String createStartDate, @RequestParam(required = false) String createEndDate) { + return R.data(preSettlementService.candidateDetailsByCreateTime(Condition.getPage(query), contractId, + settlementType, batchNo, createStartDate, createEndDate)); } @PostMapping("/save") - @ApiOperationSupport(order = 6) + @ApiOperationSupport(order = 8) @Operation(summary = "保存正式结算草稿") public R save(@RequestBody FormalSettlementSaveRequest request) { return R.data(formalSettlementService.saveDraft(request)); } @PostMapping("/remove") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 9) @Operation(summary = "删除正式结算草稿") public R remove(@RequestParam Long id) { formalSettlementService.removeDraft(id); return R.success("删除成功"); } @PostMapping("/submit") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 10) @Operation(summary = "提交审批") public R submit(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.submit(request); return R.success("提交成功"); } @PostMapping("/approve") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 11) @Operation(summary = "审批通过") public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); return R.success("审批通过"); } @PostMapping("/return") - @ApiOperationSupport(order = 10) + @ApiOperationSupport(order = 12) @Operation(summary = "审批驳回") public R returnBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.returnBill(request); return R.success("已驳回"); } @PostMapping("/void") - @ApiOperationSupport(order = 11) + @ApiOperationSupport(order = 13) @Operation(summary = "作废正式结算单") public R voidBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.voidBill(request); return R.success("作废成功"); } @PostMapping("/sync-kingdee") - @ApiOperationSupport(order = 12) + @ApiOperationSupport(order = 14) @Operation(summary = "推送金蝶应付单") public R syncKingdee(@RequestParam Long id) { return R.data(formalSettlementService.syncKingdee(id)); } @GetMapping("/detail-fees") - @ApiOperationSupport(order = 13) + @ApiOperationSupport(order = 15) @Operation(summary = "正式结算货物费用快照") public R> detailFees(@RequestParam Long detailId) { return R.data(formalSettlementService.detailFees(detailId)); } @PostMapping("/adjust-detail") - @ApiOperationSupport(order = 14) + @ApiOperationSupport(order = 16) @Operation(summary = "调整草稿结算明细") public R adjustDetail(@RequestBody PreSettlementDetailAdjustRequest request) { formalSettlementService.adjustDetail(request); @@ -138,7 +153,7 @@ public class FormalSettlementController extends BladeController { } @PostMapping("/apply-payment") - @ApiOperationSupport(order = 15) + @ApiOperationSupport(order = 17) @Operation(summary = "发起尾款付款申请") public R applyPayment(@RequestBody FormalSettlementPaymentRequest request) { return R.data(formalSettlementService.applyPayment(request)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java new file mode 100644 index 0000000..936febe --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java @@ -0,0 +1,20 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; + +/** + * 正式结算合计费用 Mapper + * + * @author Chill + */ +public interface FormalSettlementSummaryFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java index 531d798..cc65b20 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -29,6 +29,7 @@ import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; public interface IFormalSettlementService extends BaseService { IPage selectPage(IPage page, FormalSettlementVO query); IPage candidatePreSettlements(IPage page, PreSettlementVO query); + String nextNo(); FormalSettlementVO detail(Long id); Long saveDraft(FormalSettlementSaveRequest request); void removeDraft(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java index 4133a41..28af8ad 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java @@ -33,13 +33,20 @@ public interface IPreSettlementService extends BaseService { PreSettlementVO detail(Long id); - List> contractOptions(String keyword); + default List> contractOptions(String keyword) { + return contractOptions(keyword, null); + } + + List> contractOptions(String keyword, Long projectId); List> feeOptions(); IPage> candidateDetails(IPage page, Long contractId, String settlementType, String batchNo, String feeStartDate, String feeEndDate); + IPage> candidateDetailsByCreateTime(IPage page, Long contractId, + String settlementType, String batchNo, String createStartDate, String createEndDate); + Long saveDraft(PreSettlementSaveRequest request); void removeDraft(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java index b808705..84a04e1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java @@ -194,10 +194,16 @@ public class CommonRouteServiceImpl extends BaseServiceImpl queryWrapper = Wrappers.lambdaQuery() .eq(CommonRoute::getIsDeleted, 0) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index fa3efc5..8e49cda 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -21,10 +21,12 @@ import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementSourceMapper; +import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; import org.springblade.transport.mapper.PreSettlementDetailMapper; import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; @@ -35,11 +37,13 @@ import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springblade.transport.pojo.entity.FormalSettlementPayment; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlementDetail; import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.Waybill; @@ -47,6 +51,7 @@ import org.springblade.transport.pojo.vo.FormalSettlementVO; import org.springblade.transport.pojo.vo.PreSettlementVO; import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.service.IPreSettlementService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.wrapper.PreSettlementWrapper; import org.springblade.core.tool.jackson.JsonUtil; @@ -55,12 +60,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -79,15 +89,18 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl numbers = list(Wrappers.lambdaQuery() + .select(FormalSettlement::getFormalSettlementNo) + .likeRight(FormalSettlement::getFormalSettlementNo, prefix)) + .stream().map(FormalSettlement::getFormalSettlementNo).toList(); + int sequence = 1; + if (!numbers.isEmpty()) { + String latest = numbers.stream().max(String::compareTo).orElse(prefix); + try { + sequence = Integer.parseInt(latest.substring(prefix.length())) + 1; + } catch (RuntimeException ignored) { + sequence = Math.toIntExact(count(Wrappers.lambdaQuery() + .likeRight(FormalSettlement::getFormalSettlementNo, prefix))) + 1; + } + } + return prefix + String.format("%04d", sequence); + } + @Override public FormalSettlementVO detail(Long id) { FormalSettlement settlement = existing(id); @@ -138,6 +171,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo))); + vo.setSummaryFees(listSummaryFees(id)); vo.setPayments(paymentMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime))); return vo; @@ -192,6 +226,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() .in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)); detailMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, id)); + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, id)); removeById(id); } @@ -312,14 +351,232 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() - .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream() - .map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + rebuildSummaryFees(settlement.getId()); + refreshSettlementAmount(settlement); + } + + private void rebuildSummaryFees(Long settlementId) { + List existingRows = listSummaryFees(settlementId); + Map existingGenerated = existingRows.stream() + .filter(row -> !Integer.valueOf(1).equals(row.getManualFlag())) + .collect(Collectors.toMap(this::summaryKey, Function.identity(), (first, second) -> first)); + List existingManualRows = existingRows.stream() + .filter(row -> Integer.valueOf(1).equals(row.getManualFlag())).toList(); + Map aggregates = new LinkedHashMap<>(); + for (FormalSettlementSource source : sourceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSource::getFormalSettlementId, settlementId))) { + List sourceSummary = preSummaryFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, source.getPreSettlementId()) + .eq(PreSettlementSummaryFee::getIsDeleted, 0)); + sourceSummary.forEach(row -> { + BigDecimal additionalAmount = Integer.valueOf(1).equals(row.getManualFlag()) + ? money(row.getSettlementAmount()) : money(row.getAdjustAmount()); + aggregates.merge(summaryKey(row.getFeeType(), row.getFeeItem()), additionalAmount, BigDecimal::add); + }); + } + Map feeTypeMap = feeTypeMap(); + for (FormalSettlementDetail detail : detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlementId))) { + List detailFees = detailFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()) + .eq(FormalSettlementDetailFee::getIsDeleted, 0)); + appendFeeAggregates(aggregates, feeTypeMap, detailFees); + } + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, settlementId) + .eq(FormalSettlementSummaryFee::getManualFlag, 0)); + int lineNo = 1; + for (Map.Entry entry : aggregates.entrySet()) { + String[] keyParts = entry.getKey().split("@@@", 2); + FormalSettlementSummaryFee old = existingGenerated.get(entry.getKey()); + FormalSettlementSummaryFee row = new FormalSettlementSummaryFee(); + row.setFormalSettlementId(settlementId); + row.setFeeType(keyParts[0]); + row.setFeeItem(keyParts.length > 1 ? keyParts[1] : ""); + row.setOriginalAmount(money(entry.getValue())); + row.setAdjustAmount(old == null ? BigDecimal.ZERO.setScale(2) : money(old.getAdjustAmount())); + row.setSettlementAmount(row.getOriginalAmount().add(row.getAdjustAmount())); + row.setRemark(old == null ? "" : old.getRemark()); + row.setManualFlag(0); + row.setLineNo(lineNo++); + summaryFeeMapper.insert(row); + } + for (FormalSettlementSummaryFee manualRow : existingManualRows) { + manualRow.setLineNo(lineNo++); + summaryFeeMapper.updateById(manualRow); + } + renumberSummaryFees(settlementId); + } + + private void appendFeeAggregates(Map aggregates, Map feeTypeMap, + List detailFees) { + for (FormalSettlementDetailFee fee : detailFees) { + Map feeItems = parseFeeItems(fee.getFeeItemsJson()); + boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + BigDecimal knownAmount = feeItems.values().stream().map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + if (!containsFreight) { + BigDecimal freightAmount = money(fee.getFreightAmount()); + aggregates.merge(summaryKey("物流配送", "运输费"), freightAmount, BigDecimal::add); + knownAmount = knownAmount.add(freightAmount); + } + feeItems.forEach((feeItem, amount) -> aggregates.merge( + summaryKey(feeTypeMap.getOrDefault(feeItem, + isFreightFeeItem(feeItem) ? "物流配送" : "其他费用"), feeItem), + money(amount), BigDecimal::add)); + BigDecimal residualAmount = money(fee.getSettlementAmountTax()).subtract(knownAmount); + if (residualAmount.signum() != 0) { + aggregates.merge(summaryKey("其他费用", "其他费用"), residualAmount, BigDecimal::add); + } + } + } + + private void applySummaryRequest(Long settlementId, List requestRows) { + if (requestRows == null) return; + Map> allowedManualFees = new LinkedHashMap<>(); + for (Map option : preSettlementService.feeOptions()) { + Set feeItems = new LinkedHashSet<>(); + if (option.get("feeItems") instanceof List values) { + values.forEach(value -> feeItems.add(String.valueOf(value))); + } + allowedManualFees.put(String.valueOf(option.get("feeType")), feeItems); + } + Map existingMap = listSummaryFees(settlementId).stream() + .collect(Collectors.toMap(FormalSettlementSummaryFee::getId, Function.identity())); + List existingManualRows = existingMap.values().stream() + .filter(row -> Integer.valueOf(1).equals(row.getManualFlag())).toList(); + Set retainedManualIds = new LinkedHashSet<>(); + int nextLineNo = existingMap.size() + 1; + for (FormalSettlementSaveRequest.SummaryFee requestRow : requestRows) { + if (Integer.valueOf(1).equals(requestRow.getManualFlag())) { + String feeType = requiredText(requestRow.getFeeType(), "费用类型"); + String feeItem = requiredText(requestRow.getFeeItem(), "费用项"); + if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) { + throw new ServiceException("费用类型与费用项不匹配或费用项已停用"); + } + FormalSettlementSummaryFee row = requestRow.getId() == null ? null : existingMap.get(requestRow.getId()); + if (row == null && requestRow.getId() == null) row = new FormalSettlementSummaryFee(); + if (row == null || row.getId() != null && !Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的手工费用行"); + } + row.setFormalSettlementId(settlementId); + row.setFeeType(feeType); + row.setFeeItem(feeItem); + row.setOriginalAmount(BigDecimal.ZERO.setScale(2)); + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + row.setSettlementAmount(row.getAdjustAmount()); + row.setRemark(limit(requestRow.getRemark(), 50)); + row.setManualFlag(1); + if (row.getId() == null) { + row.setLineNo(nextLineNo++); + summaryFeeMapper.insert(row); + } else { + summaryFeeMapper.updateById(row); + } + retainedManualIds.add(row.getId()); + continue; + } + FormalSettlementSummaryFee row = existingMap.get(requestRow.getId()); + if (row == null) { + row = existingMap.values().stream() + .filter(item -> !Integer.valueOf(1).equals(item.getManualFlag())) + .filter(item -> Objects.equals(summaryKey(item), + summaryKey(requestRow.getFeeType(), requestRow.getFeeItem()))) + .findFirst().orElse(null); + } + if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的结算合计行"); + } + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + row.setSettlementAmount(money(row.getOriginalAmount()).add(row.getAdjustAmount())); + row.setRemark(limit(requestRow.getRemark(), 50)); + summaryFeeMapper.updateById(row); + } + for (FormalSettlementSummaryFee manualRow : existingManualRows) { + if (!retainedManualIds.contains(manualRow.getId())) summaryFeeMapper.deleteById(manualRow.getId()); + } + renumberSummaryFees(settlementId); + } + + private void refreshSettlementAmount(FormalSettlement settlement) { + BigDecimal amount = listSummaryFees(settlement.getId()).stream() + .map(FormalSettlementSummaryFee::getSettlementAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); settlement.setSettlementAmount(amount); - settlement.setLocalSettlementAmount(amount.multiply(settlement.getExchangeRate() == null ? BigDecimal.ONE : settlement.getExchangeRate())); + settlement.setLocalSettlementAmount(amount.multiply( + settlement.getExchangeRate() == null ? BigDecimal.ONE : settlement.getExchangeRate())); updateById(settlement); } + private List listSummaryFees(Long settlementId) { + return summaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, settlementId) + .eq(FormalSettlementSummaryFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementSummaryFee::getLineNo)); + } + + private void renumberSummaryFees(Long settlementId) { + List rows = listSummaryFees(settlementId); + for (int index = 0; index < rows.size(); index++) { + FormalSettlementSummaryFee row = rows.get(index); + row.setLineNo(index + 1); + summaryFeeMapper.updateById(row); + } + } + + private Map feeTypeMap() { + Map result = new LinkedHashMap<>(); + for (Map option : preSettlementService.feeOptions()) { + if (option.get("feeItems") instanceof List values) { + values.forEach(value -> result.putIfAbsent(String.valueOf(value), + String.valueOf(option.get("feeType")))); + } + } + return result; + } + + private Map parseFeeItems(String json) { + Map result = new LinkedHashMap<>(); + if (Func.isEmpty(json)) return result; + try { + Object parsed = JsonUtil.parse(json, Map.class); + if (parsed instanceof Map map) { + map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value))); + } + } catch (RuntimeException ignored) { + // 兼容历史费用JSON,不影响正式结算保存。 + } + return result; + } + + private BigDecimal decimal(Object value) { + if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO; + try { + return new BigDecimal(String.valueOf(value)); + } catch (NumberFormatException ignored) { + return BigDecimal.ZERO; + } + } + + private boolean isFreightFeeItem(String name) { + return name != null && (name.contains("运费") || name.contains("运输费")); + } + + private String summaryKey(FormalSettlementSummaryFee row) { + return summaryKey(row.getFeeType(), row.getFeeItem()); + } + + private String summaryKey(String feeType, String feeItem) { + return String.valueOf(feeType) + "@@@" + String.valueOf(feeItem); + } + + private String requiredText(String value, String field) { + if (Func.isEmpty(value)) throw new ServiceException(field + "不能为空"); + return value.trim(); + } + private void rebuildSnapshots(FormalSettlement settlement, List sources, List directDetails) { List existingDetailIds = detailMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream().map(FormalSettlementDetail::getId).toList(); @@ -490,12 +747,6 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery().likeRight(FormalSettlement::getFormalSettlementNo, prefix)); - return prefix + String.format("%04d", count + 1); - } - private synchronized String nextPaymentNo() { String prefix = "FK" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = paymentMapper.selectCount(Wrappers.lambdaQuery() @@ -503,7 +754,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl> contractOptions(String keyword) { + public List> contractOptions(String keyword, Long projectId) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(ContractManage::getIsDeleted, 0) .in(ContractManage::getApprovalStatus, "approved", "change_approved") .ne(ContractManage::getContractStage, "terminated") + .eq(projectId != null, ContractManage::getProjectId, projectId) .and(Func.isNotEmpty(keyword), query -> query.like(ContractManage::getContractName, keyword) .or().like(ContractManage::getContractNo, keyword)) .orderByDesc(ContractManage::getCreateTime); @@ -201,12 +202,28 @@ public class PreSettlementServiceImpl extends BaseServiceImpl> candidateDetails(IPage page, Long contractId, String settlementType, String batchNo, String feeStartDate, String feeEndDate) { + return candidateDetails(page, contractId, settlementType, batchNo, feeStartDate, feeEndDate, + false, true); + } + + @Override + public IPage> candidateDetailsByCreateTime(IPage page, Long contractId, + String settlementType, String batchNo, String createStartDate, String createEndDate) { + return candidateDetails(page, contractId, settlementType, batchNo, createStartDate, createEndDate, + true, false); + } + + private IPage> candidateDetails(IPage page, Long contractId, String settlementType, + String batchNo, String startDate, String endDate, boolean byCreateTime, + boolean validateContractType) { if (contractId == null) { throw new ServiceException("请先选择合同"); } validateSettlementType(settlementType); ContractManage contract = loadAvailableContract(contractId); - if (!Objects.equals(contractSettlementType(contract), settlementType)) { + // 正式结算需兼容历史单据及预结算转正式结算时保留的结算类型, + // 候选数据仍由合同、项目、组织、结算类型及未结算状态共同约束。 + if (validateContractType && !Objects.equals(contractSettlementType(contract), settlementType)) { throw new ServiceException("结算类型与合同类别不一致"); } LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() @@ -223,8 +240,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl query.eq(ReceivablePayableDetail::getCustomerName, contract.getPartyA()) .or().eq(ReceivablePayableDetail::getCustomerName, contract.getPartyB())) .like(Func.isNotEmpty(batchNo), ReceivablePayableDetail::getBatchNo, batchNo) - .ge(Func.isNotEmpty(feeStartDate), ReceivablePayableDetail::getFeeDate, parseDate(feeStartDate)) - .le(Func.isNotEmpty(feeEndDate), ReceivablePayableDetail::getFeeDate, parseDate(feeEndDate)) + .ge(!byCreateTime && Func.isNotEmpty(startDate), ReceivablePayableDetail::getFeeDate, + parseDate(startDate)) + .le(!byCreateTime && Func.isNotEmpty(endDate), ReceivablePayableDetail::getFeeDate, + parseDate(endDate)) + .ge(byCreateTime && Func.isNotEmpty(startDate), ReceivablePayableDetail::getCreateTime, + byCreateTime && Func.isNotEmpty(startDate) ? parseDate(startDate).atStartOfDay() : null) + .lt(byCreateTime && Func.isNotEmpty(endDate), ReceivablePayableDetail::getCreateTime, + byCreateTime && Func.isNotEmpty(endDate) ? parseDate(endDate).plusDays(1).atStartOfDay() : null) .orderByDesc(ReceivablePayableDetail::getCreateTime); IPage sourcePage = sourceDetailMapper.selectPage( new Page<>(page.getCurrent(), page.getSize()), wrapper); @@ -613,30 +636,46 @@ public class PreSettlementServiceImpl extends BaseServiceImpl candidateMap(ReceivablePayableDetail source) { Map result = new LinkedHashMap<>(); + Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId()); result.put("id", source.getId()); result.put("documentNo", source.getDocumentNo()); + result.put("createTime", source.getCreateTime()); + result.put("projectName", source.getProjectName()); + result.put("deptName", source.getDeptName()); result.put("feeDate", source.getFeeDate()); result.put("customerName", source.getCustomerName()); - result.put("departureAddress", waybillValue(source.getWaybillId(), Waybill::getDepartureAddress)); - result.put("arrivalAddress", waybillValue(source.getWaybillId(), Waybill::getArrivalAddress)); + result.put("contractNo", source.getContractNo()); result.put("contractName", source.getContractName()); + result.put("sourceType", source.getSourceType()); + result.put("preSettlementNo", source.getPreSettlementNo()); + result.put("formalSettlementNo", source.getFormalSettlementNo()); result.put("waybillNo", source.getWaybillNo()); result.put("vehicleNo", source.getVehicleNo()); + result.put("departureAddress", waybill == null ? "" : + firstNotEmpty(waybill.getDepartureAddress(), waybill.getDepartureName())); + result.put("arrivalAddress", waybill == null ? "" : + firstNotEmpty(waybill.getArrivalAddress(), waybill.getArrivalName())); + result.put("actualDepartureTime", waybill == null || waybill.getStartDate() == null ? null : + waybill.getStartDate().atStartOfDay()); + result.put("actualCompletionTime", waybill == null || waybill.getEndDate() == null ? null : + waybill.getEndDate().atStartOfDay()); result.put("transportType", source.getTransportType()); result.put("cargoName", source.getCargoName()); result.put("cargoType", source.getCargoType()); + result.put("transportQuantity", source.getTransportQuantity()); + result.put("quantityUnit", source.getQuantityUnit()); + result.put("mileage", source.getMileage()); result.put("batchNo", source.getBatchNo()); + result.put("unitPrice", source.getUnitPrice()); + result.put("freightAmount", money(source.getFreightAmount())); + result.put("feeItemsJson", source.getFeeItemsJson()); result.put("totalAmount", money(source.getTotalAmount())); result.put("currency", source.getCurrency()); + result.put("settlementStatusName", "待结算"); + result.put("remark", source.getRemark()); return result; } - private String waybillValue(Long waybillId, Function getter) { - if (waybillId == null) return ""; - Waybill waybill = waybillService.getById(waybillId); - return waybill == null ? "" : getter.apply(waybill); - } - private void synchronizeDetails(PreSettlement settlement, List requestedIds, boolean allowSourceMismatch) { List distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList(); List existingDetails = listDetails(settlement.getId()); diff --git a/doc/sql/transport/blade_common_route_region_id_patch_20260823.sql b/doc/sql/transport/blade_common_route_region_id_patch_20260823.sql new file mode 100644 index 0000000..7f2ddb8 --- /dev/null +++ b/doc/sql/transport/blade_common_route_region_id_patch_20260823.sql @@ -0,0 +1,7 @@ +ALTER TABLE `blade_common_route` + ADD COLUMN `departure_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货省ID' AFTER `departure_name`, + ADD COLUMN `departure_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货市ID' AFTER `departure_province_id`, + ADD COLUMN `departure_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货区ID' AFTER `departure_city_id`, + ADD COLUMN `arrival_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货省ID' AFTER `arrival_name`, + ADD COLUMN `arrival_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货市ID' AFTER `arrival_province_id`, + ADD COLUMN `arrival_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货区ID' AFTER `arrival_city_id`; diff --git a/doc/sql/transport/blade_formal_settlement_20260818.sql b/doc/sql/transport/blade_formal_settlement_20260818.sql index bc05c11..ab73396 100644 --- a/doc/sql/transport/blade_formal_settlement_20260818.sql +++ b/doc/sql/transport/blade_formal_settlement_20260818.sql @@ -62,6 +62,22 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_detail_fee` ( KEY `idx_formal_detail_fee_detail` (`formal_settlement_detail_id`), KEY `idx_formal_detail_fee_source` (`source_fee_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算货物费用快照'; +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_summary_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', + `line_no` int(11) NOT NULL COMMENT '行号', `fee_type` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型', + `fee_item` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `remark` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `manual_flag` int(11) NOT NULL DEFAULT '0' COMMENT '是否手工添加', PRIMARY KEY (`id`) USING BTREE, + KEY `idx_formal_settlement_summary_bill` (`formal_settlement_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算合计费用'; + CREATE TABLE IF NOT EXISTS `blade_formal_settlement_payment` ( `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, diff --git a/doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql b/doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql new file mode 100644 index 0000000..9f96b53 --- /dev/null +++ b/doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql @@ -0,0 +1,24 @@ +-- 正式结算新增结算合计费用表 + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_summary_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', + `line_no` int(11) NOT NULL COMMENT '行号', + `fee_type` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型', + `fee_item` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `remark` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `manual_flag` int(11) NOT NULL DEFAULT '0' COMMENT '是否手工添加', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_formal_settlement_summary_bill` (`formal_settlement_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算合计费用'; diff --git a/doc/sql/transport/blade_tms_business.sql b/doc/sql/transport/blade_tms_business.sql index f6f44c8..a892e3c 100644 --- a/doc/sql/transport/blade_tms_business.sql +++ b/doc/sql/transport/blade_tms_business.sql @@ -18,6 +18,9 @@ CREATE TABLE `blade_common_route` ( `route_name` varchar(100) DEFAULT NULL COMMENT '线路名称', `departure_address_id` bigint(20) DEFAULT NULL COMMENT '发货地址ID', `departure_name` varchar(100) DEFAULT NULL COMMENT '发货地', + `departure_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货省ID', + `departure_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货市ID', + `departure_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货区ID', `departure_address` varchar(255) DEFAULT NULL COMMENT '发货地址', `departure_longitude` decimal(18,6) DEFAULT NULL COMMENT '发货经度', `departure_latitude` decimal(18,6) DEFAULT NULL COMMENT '发货纬度', @@ -25,6 +28,9 @@ CREATE TABLE `blade_common_route` ( `departure_phone` varchar(100) DEFAULT NULL COMMENT '发货联系方式', `arrival_address_id` bigint(20) DEFAULT NULL COMMENT '收货地址ID', `arrival_name` varchar(100) DEFAULT NULL COMMENT '收货地', + `arrival_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货省ID', + `arrival_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货市ID', + `arrival_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货区ID', `arrival_address` varchar(255) DEFAULT NULL COMMENT '收货地址', `arrival_longitude` decimal(18,6) DEFAULT NULL COMMENT '收货经度', `arrival_latitude` decimal(18,6) DEFAULT NULL COMMENT '收货纬度', From 8ad8b2df90047eb83f5cf87b7383ea6a456bf797 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 24 Aug 2026 12:10:29 +0800 Subject: [PATCH 038/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=85=8D=E8=BD=BD?= =?UTF-8?q?=E3=80=81=E6=80=BB=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IReceivablePayableDetailService.java | 4 +- .../impl/LoadingManageServiceImpl.java | 4 +- .../ReceivablePayableDetailServiceImpl.java | 147 ++++++++++++++++-- 3 files changed, 142 insertions(+), 13 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index cf922ac..b5150b0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -77,8 +77,8 @@ public interface IReceivablePayableDetailService extends BaseService waybillIds); - /** 完成配载单后按其记录的承运商合同自动生成应付明细。 */ - void generateForCompletedLoading(List waybillIds, Long carrierContractId); + /** 完成配载单后按其记录的承运商合同汇总生成一条应付明细。 */ + void generateForCompletedLoading(List waybillIds, Long carrierContractId, String loadingNo); /** 关闭总单调度后生成总单客户合同应收,并按所属运单记录的承运商合同生成应付。 */ void generateForClosedMasterOrder(MasterOrder masterOrder); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index a6b2113..b8550c2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -300,7 +300,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl()); if (result) { receivablePayableDetailService.generateForCompletedLoading( - associatedWaybillIds, loadingManage.getCarrierContractId()); + associatedWaybillIds, loadingManage.getCarrierContractId(), loadingManage.getLoadingNo()); } return result; } @@ -338,7 +338,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIds, Long carrierContractId) { + public void generateForCompletedLoading(List waybillIds, Long carrierContractId, String loadingNo) { + if (Func.isEmpty(loadingNo)) { + throw new ServiceException("配载单号不能为空"); + } ContractManage carrierContract = carrierContractId == null ? null : contractManageService.getById(carrierContractId); if (carrierContractId != null && (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory()))) { throw new ServiceException("配载单记录的承运商合同不存在或合同类别不正确"); } - generateAutomaticWaybillDetails(loadWaybills(waybillIds), true, - waybill -> carrierContract, AuthUtil.getUserId(), new Date()); + List waybills = loadWaybills(waybillIds); + if (waybills.isEmpty()) return; + Long currentUserId = AuthUtil.getUserId(); + Date generateTime = new Date(); + generateAutomaticWaybillDetails(waybills, true, waybill -> null, currentUserId, generateTime); + generateAutomaticDetail(carrierContract, "payable", + () -> existsByLoading(loadingNo, "payable"), waybills, + (fees, unitPrice) -> { + normalizeWaybillFeeLines(fees); + return buildLoadingDetail(loadingNo, carrierContract, waybills, fees, unitPrice); + }, + false, currentUserId, generateTime); } private List loadWaybills(List waybillIds) { @@ -679,13 +693,23 @@ public class ReceivablePayableDetailServiceImpl Map carrierContracts = contractManageService.listByIds(waybills.stream() .map(Waybill::getCarrierContractId).distinct().toList()).stream() .collect(Collectors.toMap(ContractManage::getId, contract -> contract)); - generateAutomaticWaybillDetails(waybills, false, waybill -> { - ContractManage carrierContract = carrierContracts.get(waybill.getCarrierContractId()); + Map> carrierWaybills = waybills.stream().collect(Collectors.groupingBy( + Waybill::getCarrierContractId, LinkedHashMap::new, Collectors.toList())); + for (Map.Entry> entry : carrierWaybills.entrySet()) { + ContractManage carrierContract = carrierContracts.get(entry.getKey()); if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) { - throw new ServiceException("运单【" + waybill.getWaybillNo() + "】记录的承运商合同不存在或合同类别不正确"); + throw new ServiceException("总单【" + masterOrder.getMasterNo() + "】记录的承运商合同不存在或合同类别不正确"); } - return carrierContract; - }, currentUserId, generateTime); + List currentCarrierWaybills = entry.getValue(); + generateAutomaticDetail(carrierContract, "payable", + () -> existsByMasterOrderContract(masterOrder.getMasterNo(), "payable", carrierContract.getId()), + currentCarrierWaybills, + (fees, unitPrice) -> { + normalizeWaybillFeeLines(fees); + return buildMasterOrderPayableDetail(masterOrder, carrierContract, + currentCarrierWaybills, fees, unitPrice); + }, false, currentUserId, generateTime); + } } private boolean isAutomaticContract(ContractManage contract, String settlementType) { @@ -972,6 +996,46 @@ public class ReceivablePayableDetailServiceImpl return detail; } + private ReceivablePayableDetail buildLoadingDetail(String loadingNo, ContractManage contract, + List waybills, + List fees, BigDecimal unitPrice) { + Waybill firstWaybill = waybills.get(0); + ReceivablePayableDetail detail = buildDetail(firstWaybill, contract, "payable", fees, unitPrice); + detail.setSourceType(SOURCE_LOADING_ORDER); + detail.setWaybillId(null); + detail.setWaybillNo(loadingNo); + detail.setCargoName(joinWaybillField(waybills, Waybill::getCargoName)); + detail.setCargoType(joinWaybillField(waybills, Waybill::getCargoType)); + detail.setTransportQuantity(waybills.stream().map(Waybill::getQuantity) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setQuantityUnit(commonWaybillValue(waybills, Waybill::getQuantityUnit)); + detail.setBatchNo(commonWaybillValue(waybills, Waybill::getBatchNo)); + return detail; + } + + private ReceivablePayableDetail buildMasterOrderPayableDetail(MasterOrder masterOrder, + ContractManage contract, List waybills, + List fees, BigDecimal unitPrice) { + ReceivablePayableDetail detail = buildLoadingDetail( + masterOrder.getMasterNo(), contract, waybills, fees, unitPrice); + detail.setSourceType(SOURCE_MASTER_ORDER); + detail.setTransportType(masterOrder.getTransportOrganizationType()); + detail.setRemark(masterOrder.getRemark()); + return detail; + } + + private String joinWaybillField(List waybills, + java.util.function.Function getter) { + return waybills.stream().map(getter).filter(Func::isNotEmpty).distinct() + .collect(Collectors.joining(",")); + } + + private String commonWaybillValue(List waybills, + java.util.function.Function getter) { + List values = waybills.stream().map(getter).filter(Func::isNotEmpty).distinct().toList(); + return values.size() == 1 ? values.get(0) : ""; + } + private List masterOrderGoods(MasterOrder masterOrder) { List result = new ArrayList<>(); for (Map goods : parseList(masterOrder.getGoodsJson())) { @@ -1013,6 +1077,12 @@ public class ReceivablePayableDetailServiceImpl } } + private void normalizeWaybillFeeLines(List fees) { + for (int index = 0; index < fees.size(); index++) { + fees.get(index).setLineNo(String.format("%04d", index + 1)); + } + } + private String joinMasterGoodsField(List masterGoods, java.util.function.Function getter) { return masterGoods.stream().map(getter).filter(Func::isNotEmpty).distinct() @@ -1616,7 +1686,15 @@ public class ReceivablePayableDetailServiceImpl private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) { if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) { - rebuildMasterOrderDetailFee(detail, billingPlanId); + if ("payable".equals(detail.getSettlementType())) { + rebuildAggregatedWaybillDetailFee(detail, billingPlanId, "总单关联运单不存在"); + } else { + rebuildMasterOrderDetailFee(detail, billingPlanId); + } + return; + } + if (SOURCE_LOADING_ORDER.equals(detail.getSourceType())) { + rebuildAggregatedWaybillDetailFee(detail, billingPlanId, "配载单关联运单不存在"); return; } Waybill waybill = waybillService.getById(detail.getWaybillId()); @@ -1669,6 +1747,40 @@ public class ReceivablePayableDetailServiceImpl updateById(detail); } + private void rebuildAggregatedWaybillDetailFee(ReceivablePayableDetail detail, String billingPlanId, + String missingMessage) { + List waybillIds = cargoFeeMapper.selectList(Wrappers.lambdaQuery() + .select(ReceivablePayableCargoFee::getWaybillId) + .eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .isNotNull(ReceivablePayableCargoFee::getWaybillId)) + .stream().map(ReceivablePayableCargoFee::getWaybillId).distinct().toList(); + List waybills = loadWaybills(waybillIds); + if (waybills.isEmpty()) { + throw new ServiceException(missingMessage); + } + ContractManage contract = contractManageService.getById(detail.getContractId()); + List fees = waybills.stream() + .flatMap(waybill -> calculatedFees(waybill, contract, billingPlanId).stream()).toList(); + normalizeWaybillFeeLines(fees); + cargoFeeMapper.delete(Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); + fees.forEach(fee -> { + fee.setDetailId(detail.getId()); + cargoFeeMapper.insert(fee); + }); + BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + detail.setFreightAmount(freight); + detail.setOtherFeeAmount(total.subtract(freight)); + detail.setTotalAmount(total); + detail.setUnitPrice(resolveContractUnitPrice(fees)); + detail.setFeeItemsJson(aggregateFeeItemsJson(fees)); + updateById(detail); + } + private void closeDetails(List ids, String settlementType) { if (Func.isEmpty(ids)) { throw new ServiceException("请选择需要关闭的明细"); @@ -1766,6 +1878,23 @@ public class ReceivablePayableDetailServiceImpl .eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0; } + private boolean existsByMasterOrderContract(String masterNo, String settlementType, Long contractId) { + return count(Wrappers.lambdaQuery() + .eq(ReceivablePayableDetail::getSourceType, SOURCE_MASTER_ORDER) + .eq(ReceivablePayableDetail::getWaybillNo, masterNo) + .eq(ReceivablePayableDetail::getSettlementType, settlementType) + .eq(ReceivablePayableDetail::getContractId, contractId) + .eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0; + } + + private boolean existsByLoading(String loadingNo, String settlementType) { + return count(Wrappers.lambdaQuery() + .eq(ReceivablePayableDetail::getSourceType, SOURCE_LOADING_ORDER) + .eq(ReceivablePayableDetail::getWaybillNo, loadingNo) + .eq(ReceivablePayableDetail::getSettlementType, settlementType) + .eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0; + } + private String settlementType(String value) { if (Func.isEmpty(value)) return "receivable"; if (!List.of("receivable", "payable").contains(value)) { From 19949d102a3bb6c8d72036e73d9a960c6d7dca26 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 24 Aug 2026 12:41:11 +0800 Subject: [PATCH 039/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=BA=94=E6=94=B6?= =?UTF-8?q?=E5=BA=94=E4=BB=98=E8=BD=AC=E6=AD=A3=E5=BC=8F=E7=BB=93=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/ReceivablePayableDetailServiceImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 3453194..2d49dfa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -2035,10 +2035,13 @@ public class ReceivablePayableDetailServiceImpl map.put("id", detail.getId()); map.put("documentNo", detail.getDocumentNo()); map.put("settlementType", detail.getSettlementType()); + map.put("projectId", detail.getProjectId()); map.put("projectName", detail.getProjectName()); + map.put("deptId", detail.getDeptId()); map.put("deptName", detail.getDeptName()); map.put("feeDate", detail.getFeeDate()); map.put("customerName", detail.getCustomerName()); + map.put("contractId", detail.getContractId()); map.put("contractNo", detail.getContractNo()); map.put("contractName", detail.getContractName()); map.put("preSettlementNo", detail.getPreSettlementNo()); From 172098bb59bd37e70fe50c1b42a893d6fd67d115 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 24 Aug 2026 18:03:30 +0800 Subject: [PATCH 040/114] fix bug --- .../springblade/transport/mapper/MasterOrderMapper.java | 8 ++++++++ .../transport/service/impl/MasterOrderServiceImpl.java | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MasterOrderMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MasterOrderMapper.java index 0b7862f..b279b73 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MasterOrderMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MasterOrderMapper.java @@ -1,7 +1,10 @@ /** BladeX Commercial License Agreement */ package org.springblade.transport.mapper; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.springblade.transport.pojo.entity.MasterOrder; /** @@ -10,4 +13,9 @@ import org.springblade.transport.pojo.entity.MasterOrder; * @author Chill */ public interface MasterOrderMapper extends BaseMapper { + + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT COALESCE(MAX(CAST(SUBSTRING_INDEX(master_no, '-', -1) AS UNSIGNED)), 0) " + + "FROM blade_master_order WHERE master_no LIKE CONCAT(#{prefix}, '%')") + int selectMaxSerial(@Param("prefix") String prefix); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index 782c24b..e2b5196 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -517,5 +517,9 @@ public class MasterOrderServiceImpl extends BaseServiceImpl values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return LocalDate.parse(value.substring(0, 10)); } catch (Exception exception) { throw new ServiceException("日期格式不正确"); } } private String string(Map values, String key) { return string(values, key, null); } private String string(Map values, String key, String fallback) { Object value = values.get(key); return value == null ? fallback : String.valueOf(value); } - private synchronized String nextCode() { String prefix = "DL-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"; long count = count(new LambdaQueryWrapper().likeRight(MasterOrder::getMasterNo, prefix)); return prefix + String.format("%04d", count + 1); } + private synchronized String nextCode() { + String prefix = "DL-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"; + int serial = baseMapper.selectMaxSerial(prefix) + 1; + return prefix + String.format("%04d", serial); + } } From 71114f73dc64e259bfdf6adf9b41d4f775530096 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 24 Aug 2026 21:19:53 +0800 Subject: [PATCH 041/114] fix bug --- .../transport/pojo/entity/CommonCargo.java | 3 ++ .../transport/mapper/DriverMapper.java | 15 ++++++++ .../transport/mapper/DriverMapper.xml | 22 ++++++++++++ .../service/impl/CommonCargoServiceImpl.java | 7 ++++ .../service/impl/DriverServiceImpl.java | 34 ++++++++++++++----- .../impl/PreSettlementServiceImpl.java | 13 ++++--- .../transport/wrapper/CommonCargoWrapper.java | 4 +++ 7 files changed, 86 insertions(+), 12 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonCargo.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonCargo.java index 1a7a3ce..63ff868 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonCargo.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonCargo.java @@ -22,6 +22,8 @@ */ package org.springblade.transport.pojo.entity; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -76,6 +78,7 @@ public class CommonCargo extends TenantEntity { private String packageType; @Schema(description = "货值") + @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal cargoValue; @Schema(description = "规格") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.java index 850d9b9..7250752 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.java @@ -41,6 +41,21 @@ import java.util.List; */ public interface DriverMapper extends BaseMapper { + /** + * 按身份证号查询司机(包含逻辑删除记录,用于唯一性校验)。 + */ + Driver selectByIdCardNoIncludingDeleted(@Param("idCardNo") String idCardNo); + + /** + * 按主键查询司机(包含逻辑删除记录,用于提交前校验)。 + */ + Driver selectByIdIncludingDeleted(@Param("id") Long id); + + /** + * 恢复逻辑删除司机。 + */ + int restoreById(@Param("id") Long id); + /** * 自定义分页 * diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml index 737152f..93cf5d5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml @@ -90,6 +90,28 @@ remark + + + + + + UPDATE blade_transport_driver + SET is_deleted = 0 + WHERE id = #{id} + + ( ((driving_license_long_term IS NULL OR driving_license_long_term != 1) AND driving_license_end_date < #{driver.today}) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java index b76d98f..75b51b4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java @@ -43,6 +43,7 @@ import org.springblade.transport.wrapper.CommonCargoWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -115,6 +116,7 @@ public class CommonCargoServiceImpl extends BaseServiceImpl { CommonCargoExportExcel excel = new CommonCargoExportExcel(); BeanUtil.copyProperties(record, excel); + excel.setCargoValue(normalizeCargoValue(record.getCargoValue())); excel.setPackageBrand(String.join(" / ", List.of( Func.isEmpty(record.getPackageType()) ? "" : record.getPackageType(), Func.isEmpty(record.getBrand()) ? "" : record.getBrand() @@ -255,6 +257,7 @@ public class CommonCargoServiceImpl extends BaseServiceImpl imp public boolean submit(Driver driver) { prepare(driver); validate(driver); - checkUniqueIdCard(driver); + prepareSubmitTarget(driver); return saveOrUpdate(driver); } @@ -235,14 +234,33 @@ public class DriverServiceImpl extends BaseServiceImpl imp } } - private void checkUniqueIdCard(Driver driver) { - Long count = count(Wrappers.lambdaQuery() - .eq(Driver::getIsDeleted, 0) - .eq(Driver::getIdCardNo, driver.getIdCardNo()) - .ne(Func.isNotEmpty(driver.getId()), Driver::getId, driver.getId())); - if (count > 0) { + private void prepareSubmitTarget(Driver driver) { + Driver existingById = Func.isEmpty(driver.getId()) + ? null : baseMapper.selectByIdIncludingDeleted(driver.getId()); + if (Func.isNotEmpty(driver.getId()) && existingById == null) { + throw new ServiceException("司机不存在,不能提交"); + } + Driver existingByIdCard = baseMapper.selectByIdCardNoIncludingDeleted(driver.getIdCardNo()); + if (existingByIdCard == null) { + if (existingById != null && Objects.equals(existingById.getIsDeleted(), 1)) { + throw new ServiceException("司机不存在,不能提交"); + } + return; + } + boolean sameRecord = existingById != null + && Objects.equals(existingByIdCard.getId(), existingById.getId()); + if (!Objects.equals(existingByIdCard.getIsDeleted(), 1)) { + if (!sameRecord) { + throw new ServiceException("身份证号已存在"); + } + return; + } + if (!sameRecord && Func.isNotEmpty(driver.getId())) { throw new ServiceException("身份证号已存在"); } + baseMapper.restoreById(existingByIdCard.getId()); + driver.setId(existingByIdCard.getId()); + driver.setIsDeleted(0); } private void validateLength(String value, int maxLength, String message) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 6f72064..ba740ad 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -848,12 +848,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl feeItems = parseFeeItems(feeRow.getFeeItemsJson()); boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); if (!containsFreight) { - aggregates.merge(summaryKey("物流配送", "运输费"), money(feeRow.getFreightAmount()), + aggregates.merge( + summaryKey(feeTypeMap.getOrDefault("运输费", ""), "运输费"), + money(feeRow.getFreightAmount()), BigDecimal::add); } feeItems.forEach((feeItem, amount) -> aggregates.merge( summaryKey(feeTypeMap.getOrDefault(feeItem, - isFreightFeeItem(feeItem) ? "物流配送" : "其他费用"), feeItem), + ""), feeItem), money(amount), BigDecimal::add)); } summaryFeeMapper.delete(Wrappers.lambdaQuery() @@ -884,6 +886,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl requestRows) { if (requestRows == null) return; + Map generatedFeeTypeMap = contractFeeTypeMap(loadExisting(settlementId).getContractId()); Map> allowedManualFees = new LinkedHashMap<>(); if (requestRows.stream().anyMatch(row -> Integer.valueOf(1).equals(row.getManualFlag()))) { for (Map option : feeOptions()) { @@ -935,12 +938,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl !Integer.valueOf(1).equals(item.getManualFlag())) - .filter(item -> Objects.equals(item.getFeeType(), requestRow.getFeeType()) - && Objects.equals(item.getFeeItem(), requestRow.getFeeItem())) + .filter(item -> Objects.equals(item.getFeeType(), feeType) + && Objects.equals(item.getFeeItem(), feeItem)) .findFirst().orElse(null); } if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/CommonCargoWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/CommonCargoWrapper.java index 8f148a6..c0841d3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/CommonCargoWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/CommonCargoWrapper.java @@ -30,6 +30,7 @@ import org.springblade.system.cache.UserCache; import org.springblade.transport.pojo.entity.CommonCargo; import org.springblade.transport.pojo.vo.CommonCargoVO; +import java.math.BigDecimal; import java.util.Objects; /** @@ -46,6 +47,9 @@ public class CommonCargoWrapper extends BaseEntityWrapper Date: Mon, 24 Aug 2026 23:28:30 +0800 Subject: [PATCH 042/114] fix bug --- .../common/excel/ImportFailureExcelUtil.java | 22 +++ .../system/excel/AirportMasterExcel.java | 2 +- .../system/excel/PortTerminalExcel.java | 22 +-- .../system/excel/RailwayStationExcel.java | 2 +- .../impl/AirportMasterServiceImpl.java | 173 +++++++++++++++- .../service/impl/PortTerminalServiceImpl.java | 176 ++++++++++++++++- .../impl/RailwayStationServiceImpl.java | 187 +++++++++++++++++- .../impl/AccidentRecordServiceImpl.java | 58 +++++- .../AnnualInspectionRecordServiceImpl.java | 48 ++++- .../impl/EquipmentLedgerServiceImpl.java | 56 +++++- .../service/impl/EtcRecordServiceImpl.java | 44 ++++- .../impl/InsuranceRecordServiceImpl.java | 61 +++++- .../impl/MaintenancePlanServiceImpl.java | 41 +++- .../impl/MaintenanceRecordServiceImpl.java | 41 +++- .../impl/MileageRecordServiceImpl.java | 47 ++++- .../impl/OilElectricRecordServiceImpl.java | 50 ++++- .../impl/OtherExpenseRecordServiceImpl.java | 43 +++- .../TireReplacementRecordServiceImpl.java | 39 +++- .../TransportChangeRecordServiceImpl.java | 36 +++- .../impl/ViolationRecordServiceImpl.java | 55 +++++- 20 files changed, 1146 insertions(+), 57 deletions(-) diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java index c529a25..53771af 100644 --- a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java @@ -57,6 +57,27 @@ import java.util.Objects; */ public class ImportFailureExcelUtil { + public static String formatErrorMessage(int rowNumber, List validationErrors) { + StringBuilder errorMessage = new StringBuilder("第").append(rowNumber).append("行:"); + for (int index = 0; index < validationErrors.size(); index++) { + if (index > 0) { + errorMessage.append(System.lineSeparator()); + } + errorMessage.append(index + 1).append(". ").append(validationErrors.get(index)); + } + return errorMessage.toString(); + } + + public static void addValidationError(List validationErrors, boolean invalid, String message) { + if (invalid && message != null && !message.isBlank() && !validationErrors.contains(message)) { + validationErrors.add(message); + } + } + + public static void addLengthValidationError(List validationErrors, String value, int maxLength, String message) { + addValidationError(validationErrors, value != null && value.length() > maxLength, message); + } + private static final String FAILURE_REASON = "导入失败原因"; private static final String ERROR_MESSAGE_FIELD = "errorMessage"; private static final String FAILURE_REASON_FIELD = "failureReason"; @@ -244,6 +265,7 @@ public class ImportFailureExcelUtil { Font font = workbook.createFont(); font.setColor(IndexedColors.RED.getIndex()); newStyle.setFont(font); + newStyle.setWrapText(true); return newStyle; }); cell.setCellStyle(redStyle); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExcel.java index b72d4d5..4c2c095 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExcel.java @@ -100,7 +100,7 @@ public class AirportMasterExcel implements Serializable { @ExcelProperty("备注") private String remark; - @ExcelProperty + @ExcelIgnore private String errorMessage; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java index 6e394a2..fa9f4c6 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java @@ -53,13 +53,13 @@ public class PortTerminalExcel implements Serializable { @ExcelIgnore private Long id; - @ExcelProperty("编码") + @ExcelProperty("*编码") private String code; - @ExcelProperty("港口/码头名称") + @ExcelProperty("*港口/码头名称") private String name; - @ExcelProperty("类型") + @ExcelProperty("*类型") private String category; @ExcelProperty("上级港口") @@ -68,13 +68,13 @@ public class PortTerminalExcel implements Serializable { @ExcelProperty("上级港口编码") private String parentCode; - @ExcelProperty("国家") + @ExcelProperty("*国家") private String country; - @ExcelProperty("城市") + @ExcelProperty("*城市") private String city; - @ExcelProperty("区县") + @ExcelProperty("*区县") private String districtName; @ExcelProperty("行政区划编码") @@ -83,24 +83,24 @@ public class PortTerminalExcel implements Serializable { @ExcelProperty("详细地址") private String detailAddress; - @ExcelProperty("经度") + @ExcelProperty("*经度") @NumberFormat("0.000000") private BigDecimal longitude; - @ExcelProperty("纬度") + @ExcelProperty("*纬度") @NumberFormat("0.000000") private BigDecimal latitude; - @ExcelProperty("数据来源") + @ExcelIgnore private String dataSource; - @ExcelProperty("启停状态") + @ExcelIgnore private String statusName; @ExcelProperty("备注") private String remark; - @ExcelProperty + @ExcelIgnore private String errorMessage; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExcel.java index d4915b4..0e1cd27 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExcel.java @@ -96,7 +96,7 @@ public class RailwayStationExcel implements Serializable { @ExcelProperty("备注") private String remark; - @ExcelProperty + @ExcelIgnore private String errorMessage; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java index 53dc0a5..6270287 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java @@ -43,12 +43,15 @@ import org.springblade.system.service.IAirportMasterService; import org.springblade.system.service.IRegionService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; @@ -124,24 +127,184 @@ public class AirportMasterServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List airportMasterList = new ArrayList<>(); + Map iataCodeCountMap = buildImportValueCountMap(data.stream() + .map(excel -> trimToEmpty(excel.getIataCode()).toUpperCase(Locale.ROOT)) + .toList()); + Map icaoCodeCountMap = buildImportValueCountMap(data.stream() + .map(excel -> trimToEmpty(excel.getIcaoCode()).toUpperCase(Locale.ROOT)) + .toList()); for (int index = 0; index < data.size(); index++) { AirportMasterExcel excel = data.get(index); + AirportMaster airportMaster = Objects.requireNonNull(BeanUtil.copyProperties(excel, AirportMaster.class)); + airportMaster.setDataSource(SOURCE_BATCH); + airportMaster.setStatus(STATUS_ENABLED); + normalizeImportAirportMaster(airportMaster); + List validationErrors = validateImportAirportMaster(airportMaster, iataCodeCountMap, icaoCodeCountMap); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(formatImportErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } try { - AirportMaster airportMaster = Objects.requireNonNull(BeanUtil.copyProperties(excel, AirportMaster.class)); - airportMaster.setDataSource(SOURCE_BATCH); - airportMaster.setStatus(STATUS_ENABLED); prepare(airportMaster, SOURCE_BATCH); validate(airportMaster); - save(airportMaster); + airportMasterList.add(airportMaster); } catch (Exception exception) { String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; - excel.setErrorMessage("第" + (index + 2) + "行:" + message); + excel.setErrorMessage(formatImportErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (AirportMaster airportMaster : airportMasterList) { + if (!save(airportMaster)) { + throw new ServiceException("空港机场保存失败"); + } + } return errorList; } + private Map buildImportValueCountMap(List values) { + Map valueCountMap = new HashMap<>(); + for (String value : values) { + if (Func.isNotEmpty(value)) { + valueCountMap.merge(value, 1, Integer::sum); + } + } + return valueCountMap; + } + + private void normalizeImportAirportMaster(AirportMaster airportMaster) { + airportMaster.setIataCode(trimToEmpty(airportMaster.getIataCode()).toUpperCase(Locale.ROOT)); + airportMaster.setCode(CODE_PREFIX + airportMaster.getIataCode()); + airportMaster.setIcaoCode(trimToEmpty(airportMaster.getIcaoCode()).toUpperCase(Locale.ROOT)); + airportMaster.setName(trimToEmpty(airportMaster.getName())); + airportMaster.setShortName(trimToNull(airportMaster.getShortName())); + airportMaster.setProvinceCode(trimToNull(airportMaster.getProvinceCode())); + airportMaster.setProvinceName(trimToNull(airportMaster.getProvinceName())); + airportMaster.setCityCode(trimToNull(airportMaster.getCityCode())); + airportMaster.setCityName(trimToNull(airportMaster.getCityName())); + airportMaster.setDistrictCode(trimToNull(airportMaster.getDistrictCode())); + airportMaster.setDistrictName(trimToNull(airportMaster.getDistrictName())); + airportMaster.setRegionCode(trimToNull(airportMaster.getRegionCode())); + if (Func.isEmpty(airportMaster.getDistrictCode()) && Func.isNotEmpty(airportMaster.getRegionCode())) { + airportMaster.setDistrictCode(airportMaster.getRegionCode()); + } + airportMaster.setDetailAddress(trimToNull(airportMaster.getDetailAddress())); + airportMaster.setRemark(trimToNull(airportMaster.getRemark())); + } + + private List validateImportAirportMaster(AirportMaster airportMaster, + Map iataCodeCountMap, Map icaoCodeCountMap) { + List validationErrors = new ArrayList<>(); + if (Func.isEmpty(airportMaster.getIataCode())) { + addValidationError(validationErrors, "IATA编码不能为空"); + } else { + if (!IATA_CODE_PATTERN.matcher(airportMaster.getIataCode()).matches()) { + addValidationError(validationErrors, "IATA编码为3位大写字母"); + } + if (iataCodeCountMap.getOrDefault(airportMaster.getIataCode(), 0) > 1) { + addValidationError(validationErrors, "IATA编码在本次导入中重复"); + } + validateImportUnique(AirportMaster::getIataCode, airportMaster.getIataCode(), "该IATA编码已存在", validationErrors); + validateImportUnique(AirportMaster::getCode, airportMaster.getCode(), "该编码已存在", validationErrors); + } + if (Func.isEmpty(airportMaster.getIcaoCode())) { + addValidationError(validationErrors, "ICAO代码不能为空"); + } else { + if (!ICAO_CODE_PATTERN.matcher(airportMaster.getIcaoCode()).matches()) { + addValidationError(validationErrors, "ICAO代码为4位大写字母"); + } + if (icaoCodeCountMap.getOrDefault(airportMaster.getIcaoCode(), 0) > 1) { + addValidationError(validationErrors, "ICAO代码在本次导入中重复"); + } + validateImportUnique(AirportMaster::getIcaoCode, airportMaster.getIcaoCode(), "该ICAO代码已存在", validationErrors); + } + if (Func.isEmpty(airportMaster.getName())) { + addValidationError(validationErrors, "机场标准名称不能为空"); + } + validateImportLength(airportMaster.getCode(), CODE_MAX_LENGTH, "编码不能超过20字", validationErrors); + validateImportLength(airportMaster.getName(), NAME_MAX_LENGTH, "机场标准名称不能超过100字", validationErrors); + validateImportLength(airportMaster.getShortName(), SHORT_NAME_MAX_LENGTH, "机场简称不能超过100字", validationErrors); + validateImportLength(airportMaster.getProvinceName(), REGION_NAME_MAX_LENGTH, "所属省份不能超过128字", validationErrors); + validateImportLength(airportMaster.getCityName(), REGION_NAME_MAX_LENGTH, "所属城市不能超过128字", validationErrors); + validateImportLength(airportMaster.getDistrictName(), REGION_NAME_MAX_LENGTH, "所属区县不能超过128字", validationErrors); + validateImportLength(airportMaster.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编号不能超过32字", validationErrors); + validateImportLength(airportMaster.getDetailAddress(), DETAIL_ADDRESS_MAX_LENGTH, "详细地址不能超过255字", validationErrors); + validateImportLength(airportMaster.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字", validationErrors); + if (Func.isEmpty(airportMaster.getLongitude())) { + addValidationError(validationErrors, "经度不能为空"); + } else if (!validRange(airportMaster.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE)) { + addValidationError(validationErrors, "经度范围为 -180 到 180"); + } + if (Func.isEmpty(airportMaster.getLatitude())) { + addValidationError(validationErrors, "纬度不能为空"); + } else if (!validRange(airportMaster.getLatitude(), MIN_LATITUDE, MAX_LATITUDE)) { + addValidationError(validationErrors, "纬度范围为 -90 到 90"); + } + validateImportAirportRegion(airportMaster, validationErrors); + return validationErrors; + } + + private void validateImportAirportRegion(AirportMaster airportMaster, List validationErrors) { + boolean provinceMissing = Func.isEmpty(airportMaster.getProvinceCode()) && Func.isEmpty(airportMaster.getProvinceName()); + boolean cityMissing = Func.isEmpty(airportMaster.getCityCode()) && Func.isEmpty(airportMaster.getCityName()); + boolean districtMissing = Func.isEmpty(airportMaster.getDistrictCode()) && Func.isEmpty(airportMaster.getDistrictName()); + if (provinceMissing) { + addValidationError(validationErrors, "所属省份不能为空"); + } + if (cityMissing) { + addValidationError(validationErrors, "所属城市不能为空"); + } + if (districtMissing) { + addValidationError(validationErrors, "所属区县不能为空"); + } + if (provinceMissing || cityMissing || districtMissing) { + return; + } + try { + fillRegion(airportMaster); + } catch (ServiceException exception) { + addValidationError(validationErrors, exception.getMessage()); + } + } + + private void validateImportUnique(com.baomidou.mybatisplus.core.toolkit.support.SFunction column, + String value, String message, List validationErrors) { + if (count(Wrappers.lambdaQuery() + .eq(column, value) + .eq(AirportMaster::getIsDeleted, 0)) > 0L) { + addValidationError(validationErrors, message); + } + } + + private void validateImportLength(String value, int maxLength, String message, List validationErrors) { + if (Func.isNotEmpty(value) && value.length() > maxLength) { + addValidationError(validationErrors, message); + } + } + + private void addValidationError(List validationErrors, String message) { + if (Func.isNotEmpty(message) && !validationErrors.contains(message)) { + validationErrors.add(message); + } + } + + private String formatImportErrorMessage(int rowNumber, List validationErrors) { + StringBuilder errorMessage = new StringBuilder("第").append(rowNumber).append("行:"); + for (int index = 0; index < validationErrors.size(); index++) { + if (index > 0) { + errorMessage.append(System.lineSeparator()); + } + errorMessage.append(index + 1).append(". ").append(validationErrors.get(index)); + } + return errorMessage.toString(); + } + @Override public List exportAirportMaster(Wrapper queryWrapper) { List airportMasterList = list(queryWrapper); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java index 07aaf8a..14d485c 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java @@ -43,12 +43,15 @@ import org.springblade.system.service.IPortTerminalService; import org.springblade.system.service.IRegionService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; @@ -134,24 +137,187 @@ public class PortTerminalServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List portTerminalList = new ArrayList<>(); + Map codeCountMap = buildImportCodeCountMap(data); for (int index = 0; index < data.size(); index++) { PortTerminalExcel excel = data.get(index); + PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class)); + portTerminal.setDataSource(SOURCE_BATCH); + portTerminal.setStatus(STATUS_ENABLED); + normalizeImportPortTerminal(portTerminal); + List validationErrors = validateImportPortTerminal(portTerminal, codeCountMap); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(formatImportErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } try { - PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class)); - portTerminal.setDataSource(SOURCE_BATCH); - portTerminal.setStatus(STATUS_ENABLED); prepare(portTerminal, SOURCE_BATCH); validate(portTerminal); - save(portTerminal); + portTerminalList.add(portTerminal); } catch (Exception exception) { String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; - excel.setErrorMessage("第" + (index + 2) + "行:" + message); + excel.setErrorMessage(formatImportErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (PortTerminal portTerminal : portTerminalList) { + if (!save(portTerminal)) { + throw new ServiceException("港口码头保存失败"); + } + } return errorList; } + private String formatImportErrorMessage(int rowNumber, List validationErrors) { + StringBuilder errorMessage = new StringBuilder("第").append(rowNumber).append("行:"); + for (int index = 0; index < validationErrors.size(); index++) { + if (index > 0) { + errorMessage.append(System.lineSeparator()); + } + errorMessage.append(index + 1).append(". ").append(validationErrors.get(index)); + } + return errorMessage.toString(); + } + + private Map buildImportCodeCountMap(List data) { + Map codeCountMap = new HashMap<>(); + for (PortTerminalExcel excel : data) { + String code = trimToEmpty(excel.getCode()).toUpperCase(Locale.ROOT); + if (Func.isNotEmpty(code)) { + codeCountMap.merge(code, 1, Integer::sum); + } + } + return codeCountMap; + } + + private void normalizeImportPortTerminal(PortTerminal portTerminal) { + portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT)); + portTerminal.setCategory(trimToEmpty(portTerminal.getCategory())); + portTerminal.setName(trimToEmpty(portTerminal.getName())); + portTerminal.setParentCode(trimToNull(portTerminal.getParentCode())); + if (Func.isNotEmpty(portTerminal.getParentCode())) { + portTerminal.setParentCode(portTerminal.getParentCode().toUpperCase(Locale.ROOT)); + } + portTerminal.setParentName(trimToNull(portTerminal.getParentName())); + portTerminal.setCountry(trimToEmpty(portTerminal.getCountry())); + portTerminal.setCity(trimToEmpty(portTerminal.getCity())); + portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode())); + portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName())); + portTerminal.setRegionCode(trimToNull(portTerminal.getRegionCode())); + if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isNotEmpty(portTerminal.getRegionCode())) { + portTerminal.setDistrictCode(portTerminal.getRegionCode()); + } + portTerminal.setDetailAddress(trimToNull(portTerminal.getDetailAddress())); + portTerminal.setRemark(trimToNull(portTerminal.getRemark())); + } + + private List validateImportPortTerminal(PortTerminal portTerminal, Map codeCountMap) { + List validationErrors = new ArrayList<>(); + if (!CATEGORY_PORT.equals(portTerminal.getCategory()) && !CATEGORY_TERMINAL.equals(portTerminal.getCategory())) { + addValidationError(validationErrors, "类型只能为港口或码头"); + } + if (Func.isEmpty(portTerminal.getCode())) { + addValidationError(validationErrors, "编码不能为空"); + } else { + validateImportLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字", validationErrors); + if (CATEGORY_PORT.equals(portTerminal.getCategory()) && !PORT_CODE_PATTERN.matcher(portTerminal.getCode()).matches()) { + addValidationError(validationErrors, "港口编码为5位大写字母"); + } + if (CATEGORY_TERMINAL.equals(portTerminal.getCategory()) && !TERMINAL_CODE_PATTERN.matcher(portTerminal.getCode()).matches()) { + addValidationError(validationErrors, "码头编码格式为港口编码-码头标识"); + } + if (codeCountMap.getOrDefault(portTerminal.getCode(), 0) > 1) { + addValidationError(validationErrors, "编码在本次导入中重复"); + } + if (count(Wrappers.lambdaQuery() + .eq(PortTerminal::getCode, portTerminal.getCode()) + .eq(PortTerminal::getIsDeleted, 0)) > 0L) { + addValidationError(validationErrors, "该编码已存在"); + } + } + if (Func.isEmpty(portTerminal.getName())) { + addValidationError(validationErrors, "港口/码头名称不能为空"); + } + validateImportLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字", validationErrors); + validateImportLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字", validationErrors); + validateImportLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字", validationErrors); + validateImportLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字", validationErrors); + validateImportLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字", validationErrors); + validateImportLength(portTerminal.getDetailAddress(), DETAIL_ADDRESS_MAX_LENGTH, "详细地址不能超过255字", validationErrors); + validateImportLength(portTerminal.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200个字", validationErrors); + if (Func.isEmpty(portTerminal.getLongitude())) { + addValidationError(validationErrors, "经度不能为空"); + } else if (!validRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE)) { + addValidationError(validationErrors, "经度范围为 -180 到 180"); + } + if (Func.isEmpty(portTerminal.getLatitude())) { + addValidationError(validationErrors, "纬度不能为空"); + } else if (!validRange(portTerminal.getLatitude(), MIN_LATITUDE, MAX_LATITUDE)) { + addValidationError(validationErrors, "纬度范围为 -90 到 90"); + } + if (CATEGORY_PORT.equals(portTerminal.getCategory())) { + validateImportPortRegion(portTerminal, validationErrors); + } + if (CATEGORY_TERMINAL.equals(portTerminal.getCategory())) { + validateImportParentPort(portTerminal, validationErrors); + } + return validationErrors; + } + + private void validateImportPortRegion(PortTerminal portTerminal, List validationErrors) { + if (Func.isEmpty(portTerminal.getCountry())) { + addValidationError(validationErrors, "国家不能为空"); + } + if (Func.isEmpty(portTerminal.getCity())) { + addValidationError(validationErrors, "城市不能为空"); + } + if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isEmpty(portTerminal.getDistrictName())) { + addValidationError(validationErrors, "区县不能为空"); + return; + } + try { + fillRegion(portTerminal); + } catch (ServiceException exception) { + addValidationError(validationErrors, exception.getMessage()); + } + } + + private void validateImportParentPort(PortTerminal portTerminal, List validationErrors) { + if (Func.isEmpty(portTerminal.getParentCode())) { + addValidationError(validationErrors, "上级港口编码不能为空"); + return; + } + PortTerminal parentPort = getOne(Wrappers.lambdaQuery() + .eq(PortTerminal::getCode, portTerminal.getParentCode()) + .eq(PortTerminal::getCategory, CATEGORY_PORT) + .eq(PortTerminal::getIsDeleted, 0), false); + if (Func.isEmpty(parentPort)) { + addValidationError(validationErrors, "上级港口编码对应的港口不存在"); + } else if (Func.isNotEmpty(portTerminal.getParentName()) && !Objects.equals(portTerminal.getParentName(), parentPort.getName())) { + addValidationError(validationErrors, "上级港口与上级港口编码不匹配"); + } + if (Func.isNotEmpty(portTerminal.getCode()) && !portTerminal.getCode().startsWith(portTerminal.getParentCode() + "-")) { + addValidationError(validationErrors, "码头编码必须以上级港口编码开头"); + } + } + + private void validateImportLength(String value, int maxLength, String message, List validationErrors) { + if (Func.isNotEmpty(value) && value.length() > maxLength) { + addValidationError(validationErrors, message); + } + } + + private void addValidationError(List validationErrors, String message) { + if (Func.isNotEmpty(message) && !validationErrors.contains(message)) { + validationErrors.add(message); + } + } + @Override public List exportPortTerminal(Wrapper queryWrapper) { List portTerminalList = list(queryWrapper); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java index 590113a..0b73d06 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java @@ -43,12 +43,15 @@ import org.springblade.system.service.IRailwayStationService; import org.springblade.system.service.IRegionService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; @@ -127,26 +130,196 @@ public class RailwayStationServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List railwayStationList = new ArrayList<>(); + Map tmisCodeCountMap = buildImportValueCountMap(data.stream() + .map(excel -> trimToEmpty(excel.getTmisCode())) + .toList()); + Map telegraphCodeCountMap = buildImportValueCountMap(data.stream() + .map(excel -> trimToEmpty(excel.getTelegraphCode()).toUpperCase(Locale.ROOT)) + .toList()); for (int index = 0; index < data.size(); index++) { RailwayStationExcel excel = data.get(index); + RailwayStation railwayStation = Objects.requireNonNull(BeanUtil.copyProperties(excel, RailwayStation.class)); + List validationErrors = new ArrayList<>(); + railwayStation.setLongitude(parseImportCoordinate(excel.getLongitude(), "经度", validationErrors)); + railwayStation.setLatitude(parseImportCoordinate(excel.getLatitude(), "纬度", validationErrors)); + railwayStation.setDataSource(SOURCE_BATCH); + railwayStation.setStatus(STATUS_ENABLED); + normalizeImportRailwayStation(railwayStation); + validationErrors.addAll(validateImportRailwayStation(railwayStation, tmisCodeCountMap, telegraphCodeCountMap)); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(formatImportErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } try { - RailwayStation railwayStation = Objects.requireNonNull(BeanUtil.copyProperties(excel, RailwayStation.class)); - railwayStation.setLongitude(parseCoordinate(excel.getLongitude(), "经度")); - railwayStation.setLatitude(parseCoordinate(excel.getLatitude(), "纬度")); - railwayStation.setDataSource(SOURCE_BATCH); - railwayStation.setStatus(STATUS_ENABLED); prepare(railwayStation, SOURCE_BATCH); validate(railwayStation); - save(railwayStation); + railwayStationList.add(railwayStation); } catch (Exception exception) { String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; - excel.setErrorMessage("第" + (index + 2) + "行:" + message); + excel.setErrorMessage(formatImportErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (RailwayStation railwayStation : railwayStationList) { + if (!save(railwayStation)) { + throw new ServiceException("铁路车站保存失败"); + } + } return errorList; } + private Map buildImportValueCountMap(List values) { + Map valueCountMap = new HashMap<>(); + for (String value : values) { + if (Func.isNotEmpty(value)) { + valueCountMap.merge(value, 1, Integer::sum); + } + } + return valueCountMap; + } + + private void normalizeImportRailwayStation(RailwayStation railwayStation) { + railwayStation.setTmisCode(trimToEmpty(railwayStation.getTmisCode())); + railwayStation.setCode(CODE_PREFIX + railwayStation.getTmisCode()); + railwayStation.setTelegraphCode(trimToEmpty(railwayStation.getTelegraphCode()).toUpperCase(Locale.ROOT)); + railwayStation.setName(trimToEmpty(railwayStation.getName())); + railwayStation.setProvinceCode(trimToNull(railwayStation.getProvinceCode())); + railwayStation.setProvinceName(trimToNull(railwayStation.getProvinceName())); + railwayStation.setCityCode(trimToNull(railwayStation.getCityCode())); + railwayStation.setCityName(trimToNull(railwayStation.getCityName())); + railwayStation.setDistrictCode(trimToNull(railwayStation.getDistrictCode())); + railwayStation.setDistrictName(trimToNull(railwayStation.getDistrictName())); + railwayStation.setRegionCode(trimToNull(railwayStation.getRegionCode())); + if (Func.isEmpty(railwayStation.getDistrictCode()) && Func.isNotEmpty(railwayStation.getRegionCode())) { + railwayStation.setDistrictCode(railwayStation.getRegionCode()); + } + railwayStation.setDetailAddress(trimToNull(railwayStation.getDetailAddress())); + railwayStation.setRemark(trimToNull(railwayStation.getRemark())); + } + + private BigDecimal parseImportCoordinate(String value, String name, List validationErrors) { + String trimValue = trimToEmpty(value); + if (trimValue.isEmpty()) { + return null; + } + try { + return new BigDecimal(trimValue); + } catch (NumberFormatException exception) { + addValidationError(validationErrors, name + "范围不正确"); + return null; + } + } + + private List validateImportRailwayStation(RailwayStation railwayStation, + Map tmisCodeCountMap, Map telegraphCodeCountMap) { + List validationErrors = new ArrayList<>(); + if (Func.isEmpty(railwayStation.getTmisCode())) { + addValidationError(validationErrors, "TMIS国标编码不能为空"); + } else { + if (!TMIS_CODE_PATTERN.matcher(railwayStation.getTmisCode()).matches()) { + addValidationError(validationErrors, "TMIS国标编码为5位数字"); + } + if (tmisCodeCountMap.getOrDefault(railwayStation.getTmisCode(), 0) > 1) { + addValidationError(validationErrors, "TMIS国标编码在本次导入中重复"); + } + validateImportUnique(RailwayStation::getTmisCode, railwayStation.getTmisCode(), "该TMIS国标编码已存在", validationErrors); + validateImportUnique(RailwayStation::getCode, railwayStation.getCode(), "该编码已存在", validationErrors); + } + if (Func.isEmpty(railwayStation.getTelegraphCode())) { + addValidationError(validationErrors, "电报码不能为空"); + } else { + if (!TELEGRAPH_CODE_PATTERN.matcher(railwayStation.getTelegraphCode()).matches()) { + addValidationError(validationErrors, "电报码为3位大写字母"); + } + if (telegraphCodeCountMap.getOrDefault(railwayStation.getTelegraphCode(), 0) > 1) { + addValidationError(validationErrors, "电报码在本次导入中重复"); + } + validateImportUnique(RailwayStation::getTelegraphCode, railwayStation.getTelegraphCode(), "电报码已存在", validationErrors); + } + if (Func.isEmpty(railwayStation.getName())) { + addValidationError(validationErrors, "车站名称不能为空"); + } + validateImportLength(railwayStation.getCode(), CODE_MAX_LENGTH, "编码不能超过20字", validationErrors); + validateImportLength(railwayStation.getName(), NAME_MAX_LENGTH, "车站名称不能超过50字", validationErrors); + validateImportLength(railwayStation.getProvinceName(), REGION_NAME_MAX_LENGTH, "所属省份不能超过128字", validationErrors); + validateImportLength(railwayStation.getCityName(), REGION_NAME_MAX_LENGTH, "所属城市不能超过128字", validationErrors); + validateImportLength(railwayStation.getDistrictName(), REGION_NAME_MAX_LENGTH, "所属区县不能超过128字", validationErrors); + validateImportLength(railwayStation.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编号不能超过32字", validationErrors); + validateImportLength(railwayStation.getDetailAddress(), DETAIL_ADDRESS_MAX_LENGTH, "详细地址不能超过255字", validationErrors); + validateImportLength(railwayStation.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字", validationErrors); + validateImportCoordinate(railwayStation.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE, "经度", validationErrors); + validateImportCoordinate(railwayStation.getLatitude(), MIN_LATITUDE, MAX_LATITUDE, "纬度", validationErrors); + validateImportRailwayRegion(railwayStation, validationErrors); + return validationErrors; + } + + private void validateImportRailwayRegion(RailwayStation railwayStation, List validationErrors) { + boolean provinceMissing = Func.isEmpty(railwayStation.getProvinceCode()) && Func.isEmpty(railwayStation.getProvinceName()); + boolean cityMissing = Func.isEmpty(railwayStation.getCityCode()) && Func.isEmpty(railwayStation.getCityName()); + boolean districtMissing = Func.isEmpty(railwayStation.getDistrictCode()) && Func.isEmpty(railwayStation.getDistrictName()); + if (provinceMissing) { + addValidationError(validationErrors, "所属省份不能为空"); + } + if (cityMissing) { + addValidationError(validationErrors, "所属城市不能为空"); + } + if (districtMissing) { + addValidationError(validationErrors, "所属区县不能为空"); + } + if (provinceMissing || cityMissing || districtMissing) { + return; + } + try { + fillRegion(railwayStation); + } catch (ServiceException exception) { + addValidationError(validationErrors, exception.getMessage()); + } + } + + private void validateImportCoordinate(BigDecimal value, BigDecimal min, BigDecimal max, String name, List validationErrors) { + if (Func.isNotEmpty(value) && !validRange(value, min, max)) { + addValidationError(validationErrors, name + "范围不正确"); + } + } + + private void validateImportUnique(com.baomidou.mybatisplus.core.toolkit.support.SFunction column, + String value, String message, List validationErrors) { + if (count(Wrappers.lambdaQuery() + .eq(column, value) + .eq(RailwayStation::getIsDeleted, 0)) > 0L) { + addValidationError(validationErrors, message); + } + } + + private void validateImportLength(String value, int maxLength, String message, List validationErrors) { + if (Func.isNotEmpty(value) && value.length() > maxLength) { + addValidationError(validationErrors, message); + } + } + + private void addValidationError(List validationErrors, String message) { + if (Func.isNotEmpty(message) && !validationErrors.contains(message)) { + validationErrors.add(message); + } + } + + private String formatImportErrorMessage(int rowNumber, List validationErrors) { + StringBuilder errorMessage = new StringBuilder("第").append(rowNumber).append("行:"); + for (int index = 0; index < validationErrors.size(); index++) { + if (index > 0) { + errorMessage.append(System.lineSeparator()); + } + errorMessage.append(index + 1).append(". ").append(validationErrors.get(index)); + } + return errorMessage.toString(); + } + @Override public List exportRailwayStation(Wrapper queryWrapper) { List railwayStationList = list(queryWrapper); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java index b56a3e2..d98fd41 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java @@ -45,6 +45,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; /** * 事故记录 服务实现类 @@ -62,6 +63,8 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl ACCIDENT_NATURES = Set.of("重大事故", "一般事故", "轻微事故", "其他"); + private static final Set ACCIDENT_RESPONSIBILITIES = Set.of("全部责任", "主要责任", "同等责任", "次要责任", "无责任"); @Override public IPage selectAccidentRecordPage(IPage page, AccidentRecordVO accidentRecord) { @@ -86,19 +89,64 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List accidentRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { AccidentRecordExcel excel = data.get(index); try { AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class)); - submit(accidentRecord); + prepare(accidentRecord); + List validationErrors = validateImportAccidentRecord(accidentRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + validateVehicleTypeImmutable(accidentRecord); + accidentRecordList.add(accidentRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (AccidentRecord accidentRecord : accidentRecordList) { + if (!save(accidentRecord)) { + throw new ServiceException("事故记录保存失败"); + } + } return errorList; } + private List validateImportAccidentRecord(AccidentRecord accidentRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(accidentRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(accidentRecord.getVehicleType()) && !VEHICLE.equals(accidentRecord.getVehicleType()) && !SHIP.equals(accidentRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(accidentRecord.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(accidentRecord.getAccidentDate()), "事故发生日期不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(accidentRecord.getAccidentNature()), "事故性质不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(accidentRecord.getAccidentNature()) && !ACCIDENT_NATURES.contains(accidentRecord.getAccidentNature()), "事故性质不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(accidentRecord.getAccidentResponsibility()), "事故责任不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(accidentRecord.getAccidentResponsibility()) && !ACCIDENT_RESPONSIBILITIES.contains(accidentRecord.getAccidentResponsibility()), "事故责任不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, accidentRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, accidentRecord.getAccidentLocation(), LOCATION_MAX_LENGTH, "事故发生地点不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, accidentRecord.getAccidentReasonDamage(), REASON_DAMAGE_MAX_LENGTH, "事故原因及损坏情况不能超过500字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, accidentRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, accidentRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + addImportMoneyErrors(validationErrors, accidentRecord.getDirectEconomicLoss(), "直接经济损失"); + addImportMoneyErrors(validationErrors, accidentRecord.getInsuranceClaimAmount(), "保险理赔金额"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(accidentRecord.getInsuranceClaimAmount()) && Func.isNotEmpty(accidentRecord.getDirectEconomicLoss()) && accidentRecord.getInsuranceClaimAmount().compareTo(accidentRecord.getDirectEconomicLoss()) > 0, "保险理赔金额不能超过直接经济损失金额"); + return validationErrors; + } + + private void addImportMoneyErrors(List validationErrors, BigDecimal value, String fieldName) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.stripTrailingZeros().scale() > MONEY_SCALE, fieldName + "最多保留2位小数"); + } + @Override public List exportAccidentRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(accidentRecord -> { @@ -137,9 +185,15 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List annualInspectionRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { AnnualInspectionRecordExcel excel = data.get(index); try { AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class)); - submit(annualInspectionRecord); + prepare(annualInspectionRecord); + List validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + validateVehicleTypeImmutable(annualInspectionRecord); + annualInspectionRecordList.add(annualInspectionRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (AnnualInspectionRecord annualInspectionRecord : annualInspectionRecordList) { + if (!save(annualInspectionRecord)) { + throw new ServiceException("年检记录保存失败"); + } + } return errorList; } + private List validateImportAnnualInspectionRecord(AnnualInspectionRecord annualInspectionRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(annualInspectionRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(annualInspectionRecord.getVehicleType()) && !VEHICLE.equals(annualInspectionRecord.getVehicleType()) && !SHIP.equals(annualInspectionRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(annualInspectionRecord.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, VEHICLE.equals(annualInspectionRecord.getVehicleType()) && Func.isEmpty(annualInspectionRecord.getVehicleTechnicalLevel()), "车辆技术等级不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, SHIP.equals(annualInspectionRecord.getVehicleType()) && Func.isEmpty(annualInspectionRecord.getShipInspectionType()), "船舶检验类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(annualInspectionRecord.getValidUntilDate()), "有效期截止日不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(annualInspectionRecord.getValidUntilDate()) && Func.isNotEmpty(annualInspectionRecord.getInspectionAssessmentDate()) && !annualInspectionRecord.getValidUntilDate().isAfter(annualInspectionRecord.getInspectionAssessmentDate()), "有效期截止日应大于检测评定日期"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(annualInspectionRecord.getFee()), "费用不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, annualInspectionRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, annualInspectionRecord.getPassengerTypeLevel(), PASSENGER_TYPE_LEVEL_MAX_LENGTH, "客车类型及等级不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, annualInspectionRecord.getInspectionUnit(), INSPECTION_UNIT_MAX_LENGTH, "检测评定单位不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, annualInspectionRecord.getAssessmentUnit(), ASSESSMENT_UNIT_MAX_LENGTH, "评定(复核)单位不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, annualInspectionRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, annualInspectionRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + addImportMoneyErrors(validationErrors, annualInspectionRecord.getFee(), "费用"); + return validationErrors; + } + + private void addImportMoneyErrors(List validationErrors, BigDecimal value, String fieldName) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.stripTrailingZeros().scale() > MONEY_SCALE, fieldName + "最多保留2位小数"); + } + @Override public List exportAnnualInspectionRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(annualInspectionRecord -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java index a657eac..5a89a52 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java @@ -19,8 +19,10 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; /** * 设备台账服务实现 @@ -68,19 +70,63 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List equipmentLedgerList = new ArrayList<>(); + Set importEquipmentCodes = new HashSet<>(); for (int index = 0; index < data.size(); index++) { EquipmentLedgerExcel excel = data.get(index); try { EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class)); - submit(equipmentLedger); + prepare(equipmentLedger); + if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) { + equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes)); + } + List validationErrors = validateImportEquipmentLedger(equipmentLedger); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, + !importEquipmentCodes.add(equipmentLedger.getEquipmentCode()), "设备编号在本次导入中重复"); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + validateEquipmentCodeImmutable(equipmentLedger); + equipmentLedgerList.add(equipmentLedger); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (EquipmentLedger equipmentLedger : equipmentLedgerList) { + if (!save(equipmentLedger)) { + throw new ServiceException("设备台账保存失败"); + } + } return errorList; } + private List validateImportEquipmentLedger(EquipmentLedger equipmentLedger) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, !VEHICLE.equals(equipmentLedger.getVehicleType()) && !SHIP.equals(equipmentLedger.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(equipmentLedger.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(equipmentLedger.getEquipmentCode()), "设备编号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(equipmentLedger.getEquipmentName()), "设备名称不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, equipmentLedger.getOnlineStatus() != 0 && equipmentLedger.getOnlineStatus() != 1, "是否在线不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getEquipmentCode(), EQUIPMENT_CODE_MAX_LENGTH, "设备编号格式不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getEquipmentName(), EQUIPMENT_NAME_MAX_LENGTH, "设备名称不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getEquipmentBrand(), EQUIPMENT_BRAND_MAX_LENGTH, "设备品牌不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getEquipmentType(), EQUIPMENT_TYPE_MAX_LENGTH, "设备类型不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getSpecificationModel(), SPECIFICATION_MODEL_MAX_LENGTH, "规格型号不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getOriginalEquipmentNo(), ORIGINAL_EQUIPMENT_NO_MAX_LENGTH, "原厂设备号不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, equipmentLedger.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + return validationErrors; + } + @Override public List exportEquipmentLedger(Wrapper queryWrapper) { return list(queryWrapper).stream() @@ -90,10 +136,14 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl importEquipmentCodes) { String prefix = "EQ" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); for (int sequence = 1; sequence <= 99; sequence++) { String code = prefix + String.format("%02d", sequence); - if (!exists(new LambdaQueryWrapper().eq(EquipmentLedger::getEquipmentCode, code))) { + if (!importEquipmentCodes.contains(code) && !exists(new LambdaQueryWrapper().eq(EquipmentLedger::getEquipmentCode, code))) { return code; } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java index 8dbf6a3..053907a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java @@ -64,20 +64,60 @@ public class EtcRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List etcRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { EtcRecordExcel excel = data.get(index); try { EtcRecord etcRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, EtcRecord.class)); etcRecord.setDataSource("批量导入"); - submit(etcRecord); + prepare(etcRecord); + List validationErrors = validateImportEtcRecord(etcRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + etcRecordList.add(etcRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (EtcRecord etcRecord : etcRecordList) { + if (!save(etcRecord)) { + throw new ServiceException("ETC记录保存失败"); + } + } return errorList; } + private List validateImportEtcRecord(EtcRecord etcRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(etcRecord.getVehicleNo()), "车牌号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(etcRecord.getEtcCardNo()), "ETC卡号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(etcRecord.getExitTime()), "出口时间不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(etcRecord.getTransactionAmount()), "交易金额不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(etcRecord.getEntryTime()) && Func.isNotEmpty(etcRecord.getExitTime()) && !etcRecord.getExitTime().isAfter(etcRecord.getEntryTime()), "出口时间应大于入口时间"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, etcRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, etcRecord.getEtcCardNo(), ETC_CARD_NO_MAX_LENGTH, "ETC卡号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, etcRecord.getEntryStation(), STATION_MAX_LENGTH, "入口站不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, etcRecord.getExitStation(), STATION_MAX_LENGTH, "出口站不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, etcRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, etcRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + addImportMoneyErrors(validationErrors, etcRecord.getTransactionAmount(), "交易金额"); + return validationErrors; + } + + private void addImportMoneyErrors(List validationErrors, BigDecimal value, String fieldName) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.stripTrailingZeros().scale() > MONEY_SCALE, fieldName + "最多保留" + MONEY_SCALE + "位小数"); + } + @Override public List exportEtcRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(etcRecord -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java index a6efd6e..c821f7e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java @@ -57,6 +57,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -81,6 +82,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl INSURANCE_TYPES = Set.of("交强险", "商业险", "承运人责任险", "货运险", "船舶险"); private static final Set SUPPORT_FILE_TYPES = Set.of("jpg", "jpeg", "png", "bmp"); private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?"); @@ -110,19 +112,71 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List insuranceRecordList = new ArrayList<>(); + Set importPolicyKeys = new HashSet<>(); for (int index = 0; index < data.size(); index++) { InsuranceRecordExcel excel = data.get(index); try { InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class)); - submit(insuranceRecord); + prepare(insuranceRecord); + List validationErrors = validateImportInsuranceRecord(insuranceRecord); + if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) { + String policyKey = insuranceRecord.getVehicleType() + "\u0000" + insuranceRecord.getInsuranceType() + "\u0000" + insuranceRecord.getPolicyNo(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, !importPolicyKeys.add(policyKey), "同一车船类型和保险类型下保单号在本次导入中重复"); + } + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + checkUniquePolicyNo(insuranceRecord); + insuranceRecordList.add(insuranceRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (InsuranceRecord insuranceRecord : insuranceRecordList) { + if (!save(insuranceRecord)) { + throw new ServiceException("保险记录保存失败"); + } + } return errorList; } + private List validateImportInsuranceRecord(InsuranceRecord insuranceRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(insuranceRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(insuranceRecord.getVehicleType()) && !"车辆".equals(insuranceRecord.getVehicleType()) && !"船舶".equals(insuranceRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(insuranceRecord.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(insuranceRecord.getInsuranceType()), "保险类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(insuranceRecord.getInsuranceType()) && !INSURANCE_TYPES.contains(insuranceRecord.getInsuranceType()), "保险类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(insuranceRecord.getPolicyNo()), "保单号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(insuranceRecord.getStartDate()), "开始日期不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(insuranceRecord.getEndDate()), "结束日期不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(insuranceRecord.getStartDate()) && Func.isNotEmpty(insuranceRecord.getEndDate()) && insuranceRecord.getEndDate().isBefore(insuranceRecord.getStartDate()), "结束日期不能早于开始日期"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(insuranceRecord.getPremium()), "保费不能为空"); + addImportNonNegativeError(validationErrors, insuranceRecord.getInsuredAmount(), "保额不能小于0"); + addImportNonNegativeError(validationErrors, insuranceRecord.getPremium(), "保费不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, insuranceRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, insuranceRecord.getInsuranceType(), INSURANCE_TYPE_MAX_LENGTH, "保险类型不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, insuranceRecord.getPolicyNo(), POLICY_NO_MAX_LENGTH, "保单号不能超过80字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, insuranceRecord.getInvoiceNo(), INVOICE_NO_MAX_LENGTH, "发票号不能超过80字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, insuranceRecord.getOcrTemplate(), OCR_TEMPLATE_MAX_LENGTH, "OCR识别模板不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, insuranceRecord.getPolicyFile(), POLICY_FILE_MAX_LENGTH, "保单附件不能超过1000字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, insuranceRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + return validationErrors; + } + + private void addImportNonNegativeError(List validationErrors, BigDecimal value, String message) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, message); + } + @Override public List exportInsuranceRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(insuranceRecord -> { @@ -288,6 +342,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List maintenancePlanList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { MaintenancePlanExcel excel = data.get(index); try { MaintenancePlan maintenancePlan = Objects.requireNonNull(BeanUtil.copyProperties(excel, MaintenancePlan.class)); - submit(maintenancePlan); + prepare(maintenancePlan); + List validationErrors = validateImportMaintenancePlan(maintenancePlan); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + maintenancePlanList.add(maintenancePlan); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (MaintenancePlan maintenancePlan : maintenancePlanList) { + if (!save(maintenancePlan)) { + throw new ServiceException("保养计划保存失败"); + } + } return errorList; } + private List validateImportMaintenancePlan(MaintenancePlan maintenancePlan) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenancePlan.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenancePlan.getVehicleType()) && !"车辆".equals(maintenancePlan.getVehicleType()) && !"船舶".equals(maintenancePlan.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenancePlan.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenancePlan.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenancePlan.getMaintainer(), MAINTAINER_MAX_LENGTH, "保养人不能超过20字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenancePlan.getMaintenanceItem(), MAINTENANCE_ITEM_MAX_LENGTH, "保养项目不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenancePlan.getAddress(), ADDRESS_MAX_LENGTH, "地址不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenancePlan.getRemark(), REMARK_MAX_LENGTH, "备注不能超过500字"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenancePlan.getMaintenanceTime()), "保养时间不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenancePlan.getCost()), "费用不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenancePlan.getCost()) && maintenancePlan.getCost().compareTo(BigDecimal.ZERO) < 0, "费用不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenancePlan.getMileage()) && maintenancePlan.getMileage().compareTo(BigDecimal.ZERO) < 0, "里程/航程数不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenancePlan.getNextMaintenanceMileage()) && maintenancePlan.getNextMaintenanceMileage().compareTo(BigDecimal.ZERO) < 0, "下次保养里程/航程不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenancePlan.getNextMaintenanceTime()) && Func.isNotEmpty(maintenancePlan.getMaintenanceTime()) && maintenancePlan.getNextMaintenanceTime().isBefore(maintenancePlan.getMaintenanceTime()), "下次保养时间不能早于保养时间"); + return validationErrors; + } + @Override public List exportMaintenancePlan(Wrapper queryWrapper) { return list(queryWrapper).stream().map(maintenancePlan -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java index fef55c3..5ae4cb6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java @@ -84,21 +84,58 @@ public class MaintenanceRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List maintenanceRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { MaintenanceRecordExcel excel = data.get(index); try { MaintenanceRecord maintenanceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, MaintenanceRecord.class)); maintenanceRecord.setCreateTime(null); maintenanceRecord.setUpdateTime(null); - submit(maintenanceRecord); + prepare(maintenanceRecord); + List validationErrors = validateImportMaintenanceRecord(maintenanceRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + maintenanceRecordList.add(maintenanceRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (MaintenanceRecord maintenanceRecord : maintenanceRecordList) { + if (!save(maintenanceRecord)) { + throw new ServiceException("车辆维修记录保存失败"); + } + } return errorList; } + private List validateImportMaintenanceRecord(MaintenanceRecord maintenanceRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenanceRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenanceRecord.getVehicleType()) && !"车辆".equals(maintenanceRecord.getVehicleType()) && !"船舶".equals(maintenanceRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenanceRecord.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenanceRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenanceRecord.getMaintainer(), MAINTAINER_MAX_LENGTH, "维修人不能超过20字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenanceRecord.getLocation(), LOCATION_MAX_LENGTH, "维修位置不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenanceRecord.getReplacedPart(), REPLACED_PART_MAX_LENGTH, "更换零件不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenanceRecord.getAddress(), ADDRESS_MAX_LENGTH, "地址不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, maintenanceRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过500字"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenanceRecord.getMaintenanceTime()), "维修时间不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(maintenanceRecord.getCost()), "费用不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenanceRecord.getCost()) && maintenanceRecord.getCost().compareTo(BigDecimal.ZERO) < 0, "费用不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenanceRecord.getMileage()) && maintenanceRecord.getMileage().compareTo(BigDecimal.ZERO) < 0, "里程/航程数不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(maintenanceRecord.getFactoryTime()) && Func.isNotEmpty(maintenanceRecord.getMaintenanceTime()) && maintenanceRecord.getFactoryTime().isBefore(maintenanceRecord.getMaintenanceTime()), "出厂时间不能早于维修时间"); + return validationErrors; + } + @Override public List exportMaintenanceRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(maintenanceRecord -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java index 6aa427b..1660e23 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java @@ -84,19 +84,62 @@ public class MileageRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List mileageRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { MileageRecordExcel excel = data.get(index); try { MileageRecord mileageRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, MileageRecord.class)); - submit(mileageRecord); + prepare(mileageRecord); + List validationErrors = validateImportMileageRecord(mileageRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + validateVehicleNoImmutable(mileageRecord); + mileageRecordList.add(mileageRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (MileageRecord mileageRecord : mileageRecordList) { + if (!save(mileageRecord)) { + throw new ServiceException("里程记录保存失败"); + } + } return errorList; } + private List validateImportMileageRecord(MileageRecord mileageRecord) { + List validationErrors = new ArrayList<>(); + boolean isShip = VEHICLE_TYPE_SHIP.equals(mileageRecord.getVehicleType()); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, !VEHICLE_TYPE_VEHICLE.equals(mileageRecord.getVehicleType()) && !isShip, "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(mileageRecord.getVehicleNo()), isShip ? "船号不能为空" : "车牌号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, mileageRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, isShip ? "船号不能超过30字" : "车牌号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, mileageRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, mileageRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + addImportMileageErrors(validationErrors, mileageRecord.getPreviousMonthMileage(), isShip ? "上月统计航程数" : "上月统计里程数"); + addImportMileageErrors(validationErrors, mileageRecord.getCurrentMonthMileage(), isShip ? "本月统计航程数" : "本月统计里程数"); + addImportMileageErrors(validationErrors, mileageRecord.getMonthlyMileage(), isShip ? "本月航行航程数" : "本月行驶里程数"); + addImportMileageErrors(validationErrors, mileageRecord.getTotalMileage(), isShip ? "累计航程数" : "累计行驶里程数"); + if (mileageRecord.getPreviousMonthMileage() != null && mileageRecord.getCurrentMonthMileage() != null && mileageRecord.getMonthlyMileage() != null) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, mileageRecord.getCurrentMonthMileage().subtract(mileageRecord.getPreviousMonthMileage()).compareTo(mileageRecord.getMonthlyMileage()) != 0, isShip ? "本月航行航程数应等于本月统计航程数减去上月统计航程数" : "本月行驶里程数应等于本月统计里程数减去上月统计里程数"); + } + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, mileageRecord.getTotalMileage() != null && mileageRecord.getCurrentMonthMileage() != null && mileageRecord.getTotalMileage().compareTo(mileageRecord.getCurrentMonthMileage()) < 0, isShip ? "累计航程数应大于等于本月统计航程数" : "累计行驶里程数应大于等于本月统计里程数"); + return validationErrors; + } + + private void addImportMileageErrors(List validationErrors, BigDecimal value, String fieldName) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.stripTrailingZeros().scale() > MILEAGE_SCALE, fieldName + "最多保留2位小数"); + } + @Override public List exportMileageRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(mileageRecord -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java index eab4a7e..4a3ebb9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java @@ -73,20 +73,66 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List oilElectricRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { OilElectricRecordExcel excel = data.get(index); try { OilElectricRecord oilElectricRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OilElectricRecord.class)); oilElectricRecord.setDataSource(defaultDataSource(oilElectricRecord.getDataSource())); - submit(oilElectricRecord); + prepare(oilElectricRecord); + List validationErrors = validateImportOilElectricRecord(oilElectricRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + oilElectricRecordList.add(oilElectricRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (OilElectricRecord oilElectricRecord : oilElectricRecordList) { + if (!save(oilElectricRecord)) { + throw new ServiceException("油电记录保存失败"); + } + } return errorList; } + private List validateImportOilElectricRecord(OilElectricRecord oilElectricRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(oilElectricRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(oilElectricRecord.getVehicleType()) && !VEHICLE.equals(oilElectricRecord.getVehicleType()) && !SHIP.equals(oilElectricRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(oilElectricRecord.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(oilElectricRecord.getTransactionTime()), "交易时间不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(oilElectricRecord.getFeeType()), "费用类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(oilElectricRecord.getFeeType()) && !FEE_TYPES.contains(oilElectricRecord.getFeeType()), "费用类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(oilElectricRecord.getTransactionAmount()), "交易金额不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, oilElectricRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, oilElectricRecord.getCardNo(), CARD_NO_MAX_LENGTH, "卡号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, oilElectricRecord.getCardHolder(), CARD_HOLDER_MAX_LENGTH, "持卡人不能超过20字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, oilElectricRecord.getOilProduct(), OIL_PRODUCT_MAX_LENGTH, "油品不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, oilElectricRecord.getStation(), STATION_MAX_LENGTH, "站点不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, oilElectricRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, oilElectricRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + addImportNumberErrors(validationErrors, oilElectricRecord.getTransactionAmount(), "交易金额", MONEY_SCALE); + addImportNumberErrors(validationErrors, oilElectricRecord.getUnitPrice(), "单价", MONEY_SCALE); + addImportNumberErrors(validationErrors, oilElectricRecord.getBalance(), "余额", MONEY_SCALE); + addImportNumberErrors(validationErrors, oilElectricRecord.getQuantity(), "数量", QUANTITY_SCALE); + return validationErrors; + } + + private void addImportNumberErrors(List validationErrors, BigDecimal value, String fieldName, int scale) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.stripTrailingZeros().scale() > scale, fieldName + "最多保留" + scale + "位小数"); + } + @Override public List exportOilElectricRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(oilElectricRecord -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java index dd803fc..ab1f838 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java @@ -67,20 +67,59 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List otherExpenseRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { OtherExpenseRecordExcel excel = data.get(index); try { OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class)); otherExpenseRecord.setDataSource("批量导入"); - submit(otherExpenseRecord); + prepare(otherExpenseRecord); + List validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + otherExpenseRecordList.add(otherExpenseRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (OtherExpenseRecord otherExpenseRecord : otherExpenseRecordList) { + if (!save(otherExpenseRecord)) { + throw new ServiceException("其他费用记录保存失败"); + } + } return errorList; } + private List validateImportOtherExpenseRecord(OtherExpenseRecord otherExpenseRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getExpenseDate()), "费用日期不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getExpenseType()), "费用类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(otherExpenseRecord.getExpenseType()) && !EXPENSE_TYPES.contains(otherExpenseRecord.getExpenseType()), "费用类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(otherExpenseRecord.getVehicleType()) && !VEHICLE.equals(otherExpenseRecord.getVehicleType()) && !SHIP.equals(otherExpenseRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getAmount()), "金额不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, otherExpenseRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, otherExpenseRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, otherExpenseRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + addImportMoneyErrors(validationErrors, otherExpenseRecord.getAmount(), "金额"); + return validationErrors; + } + + private void addImportMoneyErrors(List validationErrors, BigDecimal value, String fieldName) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.stripTrailingZeros().scale() > MONEY_SCALE, fieldName + "最多保留" + MONEY_SCALE + "位小数"); + } + @Override public List exportOtherExpenseRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(otherExpenseRecord -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java index 2f73ef1..1bee220 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java @@ -84,19 +84,54 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List tireReplacementRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { TireReplacementRecordExcel excel = data.get(index); try { TireReplacementRecord tireReplacementRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, TireReplacementRecord.class)); - submit(tireReplacementRecord); + prepare(tireReplacementRecord); + List validationErrors = validateImportTireReplacementRecord(tireReplacementRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + tireReplacementRecordList.add(tireReplacementRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (TireReplacementRecord tireReplacementRecord : tireReplacementRecordList) { + if (!save(tireReplacementRecord)) { + throw new ServiceException("轮胎更换记录保存失败"); + } + } return errorList; } + private List validateImportTireReplacementRecord(TireReplacementRecord tireReplacementRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(tireReplacementRecord.getVehicleNo()), "车牌号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(tireReplacementRecord.getReplacementTime()), "换胎时间不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(tireReplacementRecord.getReplacementCost()), "换胎费用不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, tireReplacementRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, tireReplacementRecord.getHandler(), HANDLER_MAX_LENGTH, "处理人不能超过20字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, tireReplacementRecord.getTireBrand(), TIRE_BRAND_MAX_LENGTH, "轮胎品牌不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, tireReplacementRecord.getReplacementDescription(), DESCRIPTION_MAX_LENGTH, "换胎说明不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, tireReplacementRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过500字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, tireReplacementRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(tireReplacementRecord.getTireQuantity()) && tireReplacementRecord.getTireQuantity() <= 0, "更换轮胎数量必须大于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(tireReplacementRecord.getReplacementCost()) && tireReplacementRecord.getReplacementCost().compareTo(BigDecimal.ZERO) < 0, "换胎费用不能小于0"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(tireReplacementRecord.getReplacementCost()) && tireReplacementRecord.getReplacementCost().stripTrailingZeros().scale() > 2, "换胎费用最多保留2位小数"); + return validationErrors; + } + @Override public List exportTireReplacementRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(tireReplacementRecord -> { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java index 184fb6b..22737b0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java @@ -84,19 +84,51 @@ public class TransportChangeRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List transportChangeRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { TransportChangeRecordExcel excel = data.get(index); try { TransportChangeRecord transportChangeRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, TransportChangeRecord.class)); - submit(transportChangeRecord); + prepare(transportChangeRecord); + List validationErrors = validateImportTransportChangeRecord(transportChangeRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + transportChangeRecordList.add(transportChangeRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (TransportChangeRecord transportChangeRecord : transportChangeRecordList) { + if (!save(transportChangeRecord)) { + throw new ServiceException("变更记录保存失败"); + } + } return errorList; } + private List validateImportTransportChangeRecord(TransportChangeRecord transportChangeRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(transportChangeRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(transportChangeRecord.getVehicleType()) && !VEHICLE.equals(transportChangeRecord.getVehicleType()) && !SHIP.equals(transportChangeRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(transportChangeRecord.getChangeItem()), "变更事项不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(transportChangeRecord.getChangeItem()) && !CHANGE_ITEMS.contains(transportChangeRecord.getChangeItem()), "变更事项不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(transportChangeRecord.getChangeContent()), "变更内容不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, transportChangeRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, transportChangeRecord.getChangeContent(), CHANGE_CONTENT_MAX_LENGTH, "变更内容不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, transportChangeRecord.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, transportChangeRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + return validationErrors; + } + @Override public List exportTransportChangeRecord(Wrapper queryWrapper) { return list(queryWrapper).stream() diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java index ed9dfe9..75200fa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java @@ -91,6 +91,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl errorList = new ArrayList<>(); + List violationRecordList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { ViolationRecordImportExcel excel = data.get(index); try { @@ -100,15 +101,65 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl validationErrors = validateImportViolationRecord(violationRecord); + if (Func.isNotEmpty(validationErrors)) { + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, validationErrors)); + errorList.add(excel); + continue; + } + validateVehicleTypeImmutable(violationRecord); + violationRecordList.add(violationRecord); } catch (Exception exception) { - excel.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(index + 2, List.of(message))); errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return errorList; + } + for (ViolationRecord violationRecord : violationRecordList) { + if (!save(violationRecord)) { + throw new ServiceException("违章记录保存失败"); + } + } return errorList; } + private List validateImportViolationRecord(ViolationRecord violationRecord) { + List validationErrors = new ArrayList<>(); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleType()), "车船类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getVehicleType()) && !"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType()), "车船类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleNo()), "车牌号/船号不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getDriverName()), "驾驶人/船长不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType()), "类型不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem()), "事项不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getViolationTime()), "时间不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getViolationTime()) && violationRecord.getViolationTime().isAfter(LocalDateTime.now()), "时间不能超过当前时间"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessStatus()), "状态不能为空"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getProcessStatus()) && !PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus()), "状态值不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessDescription()), "过程描述不能为空"); + addImportNonNegativeError(validationErrors, violationRecord.getFineAmount(), "被罚金额"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getDeductPoints()) && (violationRecord.getDeductPoints() < 0 || violationRecord.getDeductPoints() > MAX_DEDUCT_POINTS), "被扣分数范围为0-15分"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字"); + org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件不能超过1000字"); + return validationErrors; + } + + private void addImportNonNegativeError(List validationErrors, BigDecimal value, String fieldName) { + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); + } + @Override public List exportViolationRecord(Wrapper queryWrapper) { return list(queryWrapper).stream().map(violationRecord -> { From 9a4be16c0510ede649190a39239251c21fc36ac8 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 25 Aug 2026 03:25:40 +0800 Subject: [PATCH 043/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=AF=84=E5=88=86?= =?UTF-8?q?=E9=87=8F=E5=8C=96=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/entity/CreditScoreItem.java | 10 +++ .../pojo/entity/CreditScoreItemOption.java | 20 +++++ .../entity/CustomerCreditScoreDetail.java | 9 ++ .../CreditScoreQuantificationServiceImpl.java | 89 ++++++++++++++++++- .../impl/CustomerArchiveServiceImpl.java | 13 ++- .../blade_credit_score_quantification.sql | 6 ++ ...antification_base_value_patch_20260825.sql | 11 +++ ...tification_score_option_patch_20260825.sql | 13 +++ doc/sql/transport/blade_customer_archive.sql | 3 + ...core_detail_score_input_patch_20260825.sql | 48 ++++++++++ 10 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 doc/sql/transport/blade_credit_score_quantification_base_value_patch_20260825.sql create mode 100644 doc/sql/transport/blade_credit_score_quantification_score_option_patch_20260825.sql create mode 100644 doc/sql/transport/blade_customer_credit_score_detail_score_input_patch_20260825.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItem.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItem.java index 98b27ba..576c731 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItem.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItem.java @@ -72,6 +72,16 @@ public class CreditScoreItem extends TenantEntity { */ @Schema(description = "评分项目") private String itemName; + /** + * 选项类型:option-选项,score-分值 + */ + @Schema(description = "选项类型:option-选项,score-分值") + private String optionType; + /** + * 分值模式基准数值 + */ + @Schema(description = "分值模式基准数值") + private BigDecimal baseValue; /** * 分值 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItemOption.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItemOption.java index 5ebce38..de5481b 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItemOption.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreItemOption.java @@ -67,6 +67,26 @@ public class CreditScoreItemOption extends TenantEntity { */ @Schema(description = "选项描述") private String optionName; + /** + * 变化类型:increase-每增加,decrease-每减少 + */ + @Schema(description = "变化类型:increase-每增加,decrease-每减少") + private String changeType; + /** + * 变化数值 + */ + @Schema(description = "变化数值") + private BigDecimal changeValue; + /** + * 变化单位:%、件、次、项、天 + */ + @Schema(description = "变化单位:%、件、次、项、天") + private String changeUnit; + /** + * 分值类型:add-加,subtract-减 + */ + @Schema(description = "分值类型:add-加,subtract-减") + private String scoreType; /** * 分值 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScoreDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScoreDetail.java index 1e6fc4a..eae9a63 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScoreDetail.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScoreDetail.java @@ -68,6 +68,15 @@ public class CustomerCreditScoreDetail extends TenantEntity { @Schema(description = "评分项目") private String itemName; + @Schema(description = "选项类型:option-选项,score-分值") + private String optionType; + + @Schema(description = "分值模式基准数值") + private BigDecimal baseValue; + + @Schema(description = "分值模式用户输入数值") + private BigDecimal scoreInput; + @Schema(description = "评分标准") private String optionDescription; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java index 3131f4c..c4c07a2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java @@ -87,6 +87,13 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl SCORE_OPTION_UNITS = Set.of("%", "件", "次", "项", "天"); private static final String ROW_TYPE_MAIN = "主表"; private static final String ROW_TYPE_ITEM = "评分项目"; private static final String ROW_TYPE_OPTION = "选项"; @@ -253,6 +260,7 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl> itemMap = items.stream().map(item -> { CreditScoreItemVO itemVO = Objects.requireNonNull(BeanUtil.copyProperties(item, CreditScoreItemVO.class)); + itemVO.setBaseValue(normalizeBaseValue(itemVO.getBaseValue())); itemVO.setOptions(optionMap.getOrDefault(item.getId(), new ArrayList<>())); return itemVO; }).collect(Collectors.groupingBy(CreditScoreItemVO::getCategoryId)); @@ -263,6 +271,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl loadStandards(Long quantificationId) { return standardMapper.selectList(Wrappers.lambdaQuery() .eq(CreditRatingStandard::getQuantificationId, quantificationId) @@ -335,7 +347,7 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl itemMap) { @@ -475,6 +487,17 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl itemNames = new HashSet<>(); for (CreditScoreItemVO item : items) { item.setItemName(trimToEmpty(item.getItemName())); + String optionType = trimToEmpty(item.getOptionType()); + if (Func.isEmpty(optionType)) { + optionType = OPTION_TYPE_OPTION; + } + if (!OPTION_TYPE_OPTION.equals(optionType) && !OPTION_TYPE_SCORE.equals(optionType)) { + throw new ServiceException(item.getItemName() + "选项类型不正确"); + } + item.setOptionType(optionType); + if (OPTION_TYPE_SCORE.equals(optionType) && item.getBaseValue() == null) { + throw new ServiceException(item.getItemName() + "基准数值不能为空"); + } item.setScoreDescription(trimToNull(item.getScoreDescription())); item.setOptionDescription(trimToNull(item.getOptionDescription())); if (Func.isEmpty(item.getItemName())) { @@ -486,7 +509,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl 2) { + throw new ServiceException(item.getItemName() + "变化数值最多保留两位小数"); + } + if (!SCORE_OPTION_UNITS.contains(option.getChangeUnit())) { + throw new ServiceException(item.getItemName() + "变化单位不正确"); + } + if (!SCORE_TYPE_ADD.equals(option.getScoreType()) && !SCORE_TYPE_SUBTRACT.equals(option.getScoreType())) { + throw new ServiceException(item.getItemName() + "加减类型不正确"); + } + if (Func.isEmpty(option.getScore()) || option.getScore().compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException(item.getItemName() + "基础分值必须大于0"); + } + if (option.getScore().scale() > 2) { + throw new ServiceException(item.getItemName() + "基础分值最多保留两位小数"); + } + // 分值模式的选项名称由规则自动生成,避免从“选项”模式切换后残留旧名称。 + option.setOptionName(buildScoreOptionName(option)); + validateLength(option.getOptionName(), OPTION_NAME_MAX_LENGTH, "选项描述最多100字符"); + } + + private String buildScoreOptionName(CreditScoreItemOptionVO option) { + String changeLabel = CHANGE_TYPE_DECREASE.equals(option.getChangeType()) ? "每减少" : "每增加"; + String scoreLabel = SCORE_TYPE_SUBTRACT.equals(option.getScoreType()) ? "减" : "加"; + return changeLabel + formatDecimal(option.getChangeValue()) + option.getChangeUnit() + + scoreLabel + formatDecimal(option.getScore()) + "分"; + } + + private String formatDecimal(BigDecimal value) { + return value == null ? "" : value.stripTrailingZeros().toPlainString(); + } + + private void validateScale(BigDecimal value, String message) { + if (value != null && value.stripTrailingZeros().scale() > 2) { + throw new ServiceException(message); + } + } + private void validateStandards(List standards, boolean fullValidate) { if (!fullValidate && Func.isEmpty(standards)) { return; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index aa8f058..8fd928e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -668,10 +668,17 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl itemOptions = optionMap.getOrDefault(item.getId(), new ArrayList<>()); - detail.setScore(resolveFullScore(itemOptions)); + // 分值模式的项目分值是项目本身配置的基础分,选项中的 score 是阶梯变动分值。 + // 其他选项模式仍以选项最高分作为项目满分。 + BigDecimal fullScore = "score".equalsIgnoreCase(item.getOptionType()) + ? item.getScore() + : resolveFullScore(itemOptions); + detail.setScore(fullScore == null ? BigDecimal.ZERO : fullScore); detail.setOptionsJson(buildOptionsJson(itemOptions)); detail.setSelfScore(BigDecimal.ZERO); detail.setReviewScore(BigDecimal.ZERO); @@ -688,6 +695,10 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl Date: Tue, 25 Aug 2026 13:36:46 +0800 Subject: [PATCH 044/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=AF=84=E5=88=86?= =?UTF-8?q?=E9=87=8F=E5=8C=96=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreditScoreQuantificationServiceImpl.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java index c4c07a2..a49b55a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java @@ -509,8 +509,8 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl 2) { throw new ServiceException(item.getItemName() + "基础分值最多保留两位小数"); From 4e1a162660f34f1f6efa459c28882597e8c12b75 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 25 Aug 2026 16:15:19 +0800 Subject: [PATCH 045/114] fix bug --- .../common/excel/ImportFailureExcelUtil.java | 65 +++++-------------- .../system/pojo/entity/AirportMaster.java | 2 + .../system/pojo/entity/FeeItem.java | 8 +++ .../pojo/entity/CustomerArchive.java | 2 + .../controller/AirportMasterController.java | 2 +- .../controller/PortTerminalController.java | 24 +++++-- .../controller/RailwayStationController.java | 2 +- .../system/mapper/FeeItemMapper.xml | 1 + .../system/mapper/PortTerminalMapper.xml | 13 +++- .../impl/AirportMasterServiceImpl.java | 27 +++++--- .../service/impl/FeeItemServiceImpl.java | 5 ++ .../service/impl/PortTerminalServiceImpl.java | 17 ++++- .../impl/RailwayStationServiceImpl.java | 6 ++ .../impl/ContractManageServiceImpl.java | 41 ++++++++++++ .../impl/CustomerArchiveServiceImpl.java | 8 ++- .../impl/FormalSettlementServiceImpl.java | 4 ++ .../ReceivablePayableDetailServiceImpl.java | 43 ++++++++++-- doc/sql/bladex/bladex.mysql.all.create.sql | 5 +- doc/sql/transport/blade_airport_master.sql | 2 +- ..._airport_master_icao_nullable_20260825.sql | 8 +++ ...e_billing_plan_transport_mode_20260825.sql | 5 ++ doc/sql/transport/blade_fee_item.sql | 1 + .../blade_fee_item_remark_20260825.sql | 4 ++ doc/sql/transport/blade_port_terminal.sql | 2 +- ...ade_port_terminal_data_source_20260825.sql | 12 ++++ .../blade_project_contract_management.sql | 2 +- doc/sql/transport/transport.sql | 7 +- 27 files changed, 236 insertions(+), 82 deletions(-) create mode 100644 doc/sql/transport/blade_airport_master_icao_nullable_20260825.sql create mode 100644 doc/sql/transport/blade_contract_manage_billing_plan_transport_mode_20260825.sql create mode 100644 doc/sql/transport/blade_fee_item_remark_20260825.sql create mode 100644 doc/sql/transport/blade_port_terminal_data_source_20260825.sql diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java index 53771af..ced6bdf 100644 --- a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java @@ -95,6 +95,20 @@ public class ImportFailureExcelUtil { * @param excelClass 原导入 Excel 类型 */ public static void export(HttpServletResponse response, String fileName, String sheetName, List data, Class excelClass) { + exportFailureReasonOnly(response, fileName, sheetName, data, excelClass); + } + + /** + * 导出仅标红失败原因列的导入失败明细 + * + * @param response 响应 + * @param fileName 文件名 + * @param sheetName 工作表名 + * @param data 失败数据 + * @param excelClass 原导入 Excel 类型 + */ + public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName, + List data, Class excelClass) { response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); @@ -104,7 +118,7 @@ public class ImportFailureExcelUtil { List> rows = buildRows(data, excelFields); try { FastExcel.write(response.getOutputStream()) - .registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows)) + .registerWriteHandler(new ImportFailureCellStyleHandler(excelFields.size())) .head(head) .sheet(sheetName) .doWrite(rows); @@ -182,44 +196,15 @@ public class ImportFailureExcelUtil { throw new NoSuchFieldException(String.join(",", fieldNames)); } - private static String columnName(Field field) { - ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); - String[] value = excelProperty.value(); - return value.length == 0 ? field.getName() : value[0]; - } - - private static String normalize(String value) { - return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》-]", "").toLowerCase(); - } - - private static List columnKeywords(Field field) { - String columnName = columnName(field); - List keywords = new ArrayList<>(); - keywords.add(columnName); - keywords.add(field.getName()); - keywords.addAll(Arrays.asList(columnName.replace("*", "").split("[//、()()\\s]+"))); - return keywords.stream() - .map(ImportFailureExcelUtil::normalize) - .filter(keyword -> keyword.length() >= 2) - .distinct() - .toList(); - } - private static class ImportFailureCellStyleHandler implements CellWriteHandler { - private final List> columnKeywords; - private final List> rows; private final int failureReasonColumnIndex; private final Map redStyleCache = new HashMap<>(); private final Map noWrapStyleCache = new HashMap<>(); private final Map columnWidthCache = new HashMap<>(); - private ImportFailureCellStyleHandler(List excelFields, List> rows) { - this.columnKeywords = excelFields.stream() - .map(ImportFailureExcelUtil::columnKeywords) - .toList(); - this.rows = rows; - this.failureReasonColumnIndex = excelFields.size(); + private ImportFailureCellStyleHandler(int failureReasonColumnIndex) { + this.failureReasonColumnIndex = failureReasonColumnIndex; } @Override @@ -237,25 +222,11 @@ public class ImportFailureExcelUtil { return; } adjustColumnWidth(cell); - if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) { - return; - } - if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) { + if (cell.getColumnIndex() == failureReasonColumnIndex) { markRed(cell); } } - private boolean shouldMarkRed(int rowIndex, int columnIndex) { - if (columnIndex == failureReasonColumnIndex) { - return true; - } - if (columnIndex < 0 || columnIndex >= columnKeywords.size()) { - return false; - } - String failureReason = normalize(String.valueOf(rows.get(rowIndex).get(failureReasonColumnIndex))); - return columnKeywords.get(columnIndex).stream().anyMatch(failureReason::contains); - } - private void markRed(Cell cell) { CellStyle currentStyle = cell.getCellStyle(); CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/AirportMaster.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/AirportMaster.java index c0caabf..acf9bbd 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/AirportMaster.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/AirportMaster.java @@ -25,6 +25,7 @@ */ package org.springblade.system.pojo.entity; +import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; @@ -63,6 +64,7 @@ public class AirportMaster extends BaseEntity { * ICAO代码 */ @Schema(description = "ICAO代码") + @TableField(updateStrategy = FieldStrategy.ALWAYS) private String icaoCode; /** * 机场标准名称 diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/FeeItem.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/FeeItem.java index f3ef8b8..8b8e68c 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/FeeItem.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/FeeItem.java @@ -25,6 +25,8 @@ */ package org.springblade.system.pojo.entity; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -62,5 +64,11 @@ public class FeeItem extends BaseEntity { */ @Schema(description = "费用项代码") private String englishName; + /** + * 备注 + */ + @Schema(description = "备注") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String remark; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java index af2b719..434c164 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java @@ -122,9 +122,11 @@ public class CustomerArchive extends TenantEntity { private String customerLevel; @Schema(description = "最大资金使用额度(万元)") + @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal maxCreditLimit; @Schema(description = "申请总资金使用额度(万元)") + @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal applyCreditLimit; @Schema(description = "备注") diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java index caf288f..108260e 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java @@ -158,7 +158,7 @@ public class AirportMasterController extends BladeController { } List failureList = airportMasterService.importAirportMaster(ExcelUtil.read(file, AirportMasterExcel.class)); if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "空港机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AirportMasterExcel.class); + org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "空港机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AirportMasterExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java index a34396b..6e53caa 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java @@ -77,7 +77,11 @@ public class PortTerminalController extends BladeController { private static final int DEFAULT_CURRENT = 1; private static final int DEFAULT_SIZE = 10; private static final int MAX_SIZE = 100; - private static final String SOURCE_INITIAL = "初始化导入"; + private static final String SOURCE_INITIAL = "初始化录入"; + private static final String SOURCE_INITIAL_IMPORT = "初始化导入"; + private static final String SOURCE_INITIAL_OLD = "初始导入"; + private static final String SOURCE_MANUAL = "手动录入"; + private static final String SOURCE_MANUAL_OLD = "手工导入"; private final IPortTerminalService portTerminalService; @@ -95,6 +99,7 @@ public class PortTerminalController extends BladeController { if (Func.isEmpty(detail)) { return R.fail("港口码头不存在"); } + detail.setDataSource(normalizeDataSource(detail.getDataSource())); return R.data(PortTerminalWrapper.build().entityVO(detail)); } @@ -150,7 +155,9 @@ public class PortTerminalController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "上级港口下拉数据源") public R> portSelect() { - return R.data(portTerminalService.selectEnabledPorts()); + List ports = portTerminalService.selectEnabledPorts(); + ports.forEach(port -> port.setDataSource(normalizeDataSource(port.getDataSource()))); + return R.data(ports); } /** @@ -169,7 +176,7 @@ public class PortTerminalController extends BladeController { } List failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class)); if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class); + org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class); return null; } return R.success("操作成功"); @@ -237,10 +244,19 @@ public class PortTerminalController extends BladeController { return; } if (SOURCE_INITIAL.equals(value)) { - queryWrapper.in("data_source", SOURCE_INITIAL, "初始导入"); + queryWrapper.in("data_source", SOURCE_INITIAL, SOURCE_INITIAL_IMPORT, SOURCE_INITIAL_OLD); + } else if (SOURCE_MANUAL.equals(value)) { + queryWrapper.in("data_source", SOURCE_MANUAL, SOURCE_MANUAL_OLD); } else { queryWrapper.eq("data_source", value); } } + private String normalizeDataSource(String dataSource) { + if (SOURCE_INITIAL_IMPORT.equals(dataSource) || SOURCE_INITIAL_OLD.equals(dataSource)) { + return SOURCE_INITIAL; + } + return SOURCE_MANUAL_OLD.equals(dataSource) ? SOURCE_MANUAL : dataSource; + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java index 011df66..67012f5 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java @@ -161,7 +161,7 @@ public class RailwayStationController extends BladeController { } List failureList = railwayStationService.importRailwayStation(ExcelUtil.read(file, RailwayStationExcel.class)); if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "铁路车站主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RailwayStationExcel.class); + org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "铁路车站主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RailwayStationExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/FeeItemMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/FeeItemMapper.xml index fadca0b..698956a 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/FeeItemMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/FeeItemMapper.xml @@ -16,6 +16,7 @@ + + SELECT bii.*, bd.dept_name AS create_dept_name, bu.real_name AS update_user_name + FROM blade_invoice_item bii + LEFT JOIN blade_dept bd ON bd.id = bii.create_dept + LEFT JOIN blade_user bu ON bu.id = bii.update_user + WHERE bii.is_deleted = 0 + + + AND bii.short_name LIKE #{shortNameLike} + + + + AND bii.category_name LIKE #{categoryNameLike} + + + + AND bii.tax_classification_code LIKE #{taxCodeLike} + + ORDER BY bii.create_time DESC + + diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IInvoiceItemService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IInvoiceItemService.java new file mode 100644 index 0000000..e889850 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IInvoiceItemService.java @@ -0,0 +1,37 @@ +/** + * 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.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.system.pojo.entity.InvoiceItem; +import org.springblade.system.pojo.vo.InvoiceItemVO; + +/** + * 开票项目服务类 + * + * @author Chill + */ +public interface IInvoiceItemService extends BaseService { + + IPage selectInvoiceItemPage(IPage page, InvoiceItemVO invoiceItem); + + boolean submit(InvoiceItem invoiceItem); + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/InvoiceItemServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/InvoiceItemServiceImpl.java new file mode 100644 index 0000000..0974a4f --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/InvoiceItemServiceImpl.java @@ -0,0 +1,92 @@ +/** + * 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.system.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.mapper.InvoiceItemMapper; +import org.springblade.system.pojo.entity.InvoiceItem; +import org.springblade.system.pojo.vo.InvoiceItemVO; +import org.springblade.system.service.IInvoiceItemService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; + +/** + * 开票项目服务实现类 + * + * @author Chill + */ +@Service +public class InvoiceItemServiceImpl extends BaseServiceImpl + implements IInvoiceItemService { + + private static final int SHORT_NAME_MAX_LENGTH = 100; + private static final int TAX_CODE_MAX_LENGTH = 30; + private static final int CATEGORY_NAME_MAX_LENGTH = 200; + + @Override + public IPage selectInvoiceItemPage(IPage page, InvoiceItemVO invoiceItem) { + return page.setRecords(baseMapper.selectInvoiceItemPage(page, invoiceItem)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submit(InvoiceItem invoiceItem) { + prepare(invoiceItem); + validate(invoiceItem); + return saveOrUpdate(invoiceItem); + } + + private void prepare(InvoiceItem invoiceItem) { + invoiceItem.setShortName(trim(invoiceItem.getShortName())); + invoiceItem.setTaxClassificationCode(trim(invoiceItem.getTaxClassificationCode())); + invoiceItem.setCategoryName(trim(invoiceItem.getCategoryName())); + if (Func.isEmpty(invoiceItem.getStatus())) { + invoiceItem.setStatus(1); + } + } + + private void validate(InvoiceItem invoiceItem) { + if (Func.isEmpty(invoiceItem.getShortName())) throw new ServiceException("货物或服务简称不能为空"); + if (invoiceItem.getShortName().length() > SHORT_NAME_MAX_LENGTH) throw new ServiceException("货物或服务简称不能超过100字"); + if (Func.isEmpty(invoiceItem.getTaxClassificationCode())) throw new ServiceException("税收分类编码不能为空"); + if (invoiceItem.getTaxClassificationCode().length() > TAX_CODE_MAX_LENGTH) throw new ServiceException("税收分类编码不能超过30字"); + if (Func.isEmpty(invoiceItem.getCategoryName())) throw new ServiceException("商品和服务分类名称不能为空"); + if (invoiceItem.getCategoryName().length() > CATEGORY_NAME_MAX_LENGTH) throw new ServiceException("商品和服务分类名称不能超过200字"); + BigDecimal taxRate = invoiceItem.getDefaultTaxRate(); + if (taxRate == null) throw new ServiceException("默认税率不能为空"); + if (taxRate.compareTo(BigDecimal.ZERO) < 0 || taxRate.compareTo(new BigDecimal("100")) > 0) throw new ServiceException("默认税率必须在0到100之间"); + boolean duplicate = count(Wrappers.lambdaQuery() + .eq(InvoiceItem::getTaxClassificationCode, invoiceItem.getTaxClassificationCode()) + .eq(InvoiceItem::getShortName, invoiceItem.getShortName()) + .eq(InvoiceItem::getIsDeleted, 0) + .ne(Func.isNotEmpty(invoiceItem.getId()), InvoiceItem::getId, invoiceItem.getId())) > 0; + if (duplicate) throw new ServiceException("该开票项目已存在"); + } + + private String trim(String value) { + return value == null ? "" : value.trim(); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/InvoiceItemWrapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/InvoiceItemWrapper.java new file mode 100644 index 0000000..beac880 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/InvoiceItemWrapper.java @@ -0,0 +1,50 @@ +/** + * 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.system.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.SysCache; +import org.springblade.system.cache.UserCache; +import org.springblade.system.pojo.entity.InvoiceItem; +import org.springblade.system.pojo.vo.InvoiceItemVO; + +import java.util.Objects; + +/** + * 开票项目包装类 + * + * @author Chill + */ +public class InvoiceItemWrapper extends BaseEntityWrapper { + + public static InvoiceItemWrapper build() { + return new InvoiceItemWrapper(); + } + + @Override + public InvoiceItemVO entityVO(InvoiceItem invoiceItem) { + InvoiceItemVO vo = Objects.requireNonNull(BeanUtil.copyProperties(invoiceItem, InvoiceItemVO.class)); + vo.setCreateDeptName(Func.isEmpty(invoiceItem.getCreateDept()) ? "" : SysCache.getDeptName(invoiceItem.getCreateDept())); + vo.setUpdateUserName(UserCache.getUserRealName(invoiceItem.getUpdateUser())); + return vo; + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index 19f1b8e..ba1724f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -70,9 +70,11 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -183,14 +185,16 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl settlements = distinctIds(settlementIds).stream().map(this::availableSettlement).toList(); assertCompatible(settlements); FormalSettlement first = settlements.get(0); - CustomerArchive customer = findCustomer(first.getPayerName()); + CustomerArchive customer = findCustomer(first.getPayeeName()); + List invoiceInfos = customer == null ? List.of() : activeInvoiceInfos(customer.getId()); Map result = new LinkedHashMap<>(); result.put("issuerName", first.getPayeeName()); result.put("receiverName", first.getPayerName()); result.put("customer", customer); - result.put("invoiceInfos", customer == null ? List.of() : customerInvoiceInfoMapper.selectList( - Wrappers.lambdaQuery().eq(CustomerInvoiceInfo::getCustomerId, customer.getId()) - .eq(CustomerInvoiceInfo::getStatus, 1).orderByDesc(CustomerInvoiceInfo::getIsDefault))); + // 应付结算单的开票方(payeeName)是客商,受票方信息和部门邮箱均来源于该客商发票信息。 + result.put("invoices", invoiceInfos); + result.put("invoiceInfos", invoiceInfos); + result.put("departmentEmails", invoiceInfoEmails(invoiceInfos)); result.put("contacts", customer == null ? List.of() : customerContactMapper.selectList( Wrappers.lambdaQuery().eq(CustomerContact::getCustomerId, customer.getId()) .eq(CustomerContact::getStatus, 1).orderByDesc(CustomerContact::getIsDefault))); @@ -217,7 +221,6 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl settlementMap = settlements.stream() .collect(Collectors.toMap(FormalSettlement::getId, Function.identity())); BigDecimal totalAvailable = BigDecimal.ZERO; - BigDecimal allocatedTotal = BigDecimal.ZERO; for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) { FormalSettlement settlement = settlementMap.get(row.getSettlementId()); BigDecimal available = availableAmount(settlement, entity.getId()); @@ -226,21 +229,18 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl invoiceInfos = activeInvoiceInfos(customer.getId()); + CustomerInvoiceInfo invoiceInfo = invoiceInfos.stream() + .filter(item -> Objects.equals(item.getId(), request.getReceiverInvoiceInfoId())) + .findFirst().orElseThrow(() -> new ServiceException("请选择受票方有效的开票信息")); + String departmentEmails = normalizeDepartmentEmails(request.getDepartmentEmails(), invoiceInfos); String invoiceTitle = required(invoiceInfo.getInvoiceTitle(), "受票方单位"); String taxpayerNo = limit(invoiceInfo.getTaxNo(), 20, "纳税人识别号"); String bankName = limit(invoiceInfo.getBankName(), 100, "开户行"); @@ -271,7 +271,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaQuery() - .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, name).or().eq(CustomerArchive::getShortName, name)) + String value = name.trim(); + CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, value).or().eq(CustomerArchive::getShortName, value)) .eq(CustomerArchive::getStatus, 1) .eq(CustomerArchive::getIsDeleted, 0).last("limit 1")); + if (customer != null) return customer; + String normalized = normalizeCustomerName(value); + return customerArchiveMapper.selectList(Wrappers.lambdaQuery() + .eq(CustomerArchive::getStatus, 1) + .eq(CustomerArchive::getIsDeleted, 0)) + .stream() + .filter(item -> normalized.equals(normalizeCustomerName(item.getFullName())) + || normalized.equals(normalizeCustomerName(item.getShortName()))) + .findFirst().orElse(null); + } + + private String normalizeCustomerName(String value) { + return value == null ? "" : value.replaceAll("\\s+", ""); + } + + private List activeInvoiceInfos(Long customerId) { + return customerInvoiceInfoMapper.selectList(Wrappers.lambdaQuery() + .eq(CustomerInvoiceInfo::getCustomerId, customerId) + .eq(CustomerInvoiceInfo::getStatus, 1) + .eq(CustomerInvoiceInfo::getIsDeleted, 0) + .orderByDesc(CustomerInvoiceInfo::getIsDefault) + .orderByAsc(CustomerInvoiceInfo::getCreateTime)); + } + + private List invoiceInfoEmails(List invoiceInfos) { + return invoiceInfos.stream().map(CustomerInvoiceInfo::getEmail).filter(Func::isNotEmpty) + .flatMap(value -> splitEmails(value).stream()) + .collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(), + (first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList(); } private void changeStatus(Long id, String from, String to, String actionType, String node, String reason) { @@ -587,12 +617,28 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl emails = value == null ? List.of() : List.of(value.split("[;,,;]")); - List normalized = emails.stream().map(String::trim).filter(item -> !item.isEmpty()).distinct().toList(); + private String normalizeDepartmentEmails(String value, List invoiceInfos) { + Map configuredEmails = invoiceInfoEmails(invoiceInfos).stream() + .collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(), + (first, duplicate) -> first, LinkedHashMap::new)); + if (configuredEmails.isEmpty()) throw new ServiceException("当前客商的开票信息未配置邮箱"); + List normalized = splitEmails(value).stream() + .collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(), + (first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList(); if (normalized.isEmpty() || normalized.size() > 3) throw new ServiceException("部门邮箱必填且最多选择3个"); - normalized.forEach(email -> validateEmail(email, "部门邮箱")); - return String.join(";", normalized); + List selectedEmails = normalized.stream().map(email -> { + validateEmail(email, "部门邮箱"); + String configuredEmail = configuredEmails.get(email.toLowerCase(Locale.ROOT)); + if (configuredEmail == null) throw new ServiceException("部门邮箱必须选择当前客商开票信息中配置的邮箱"); + return configuredEmail; + }).distinct().toList(); + return String.join(";", selectedEmails); + } + + private List splitEmails(String value) { + if (Func.isEmpty(value)) return List.of(); + return Arrays.stream(value.split("[;,,;]")) + .map(String::trim).filter(item -> !item.isEmpty()).toList(); } private String validateEmail(String value, String name) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java index 43d1152..f6b71bb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java @@ -212,6 +212,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl oldSettlementIds = entity.getId() == null ? List.of() : relationSettlementIds(entity.getId()); KingdeeInvoicePool invoice = lockedInvoice(request.getKingdeeInvoicePoolId()); + if (invoice == null) invoice = invoiceSnapshot(request); assertInvoiceUnused(invoice, entity.getId()); Map requestedRows = distinctSettlementRows( @@ -219,10 +220,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl settlementMap = lockSettlements(requestedRows.keySet().stream().sorted().toList()); List settlements = requestedRows.keySet().stream().map(settlementMap::get).toList(); assertCompatible(settlements); - assertInvoiceParties(invoice, settlements.get(0)); - BigDecimal invoiceAmount = positive(invoice.getInvoiceAmount(), "开票金额"); - BigDecimal allocatedTotal = BigDecimal.ZERO; for (Map.Entry entry : requestedRows.entrySet()) { FormalSettlement settlement = settlementMap.get(entry.getKey()); BigDecimal allocated = nonNegative(entry.getValue().getAllocatedInvoiceAmount(), "分摊发票金额"); @@ -231,10 +229,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpllambdaQuery() + return invoicePoolMapper.selectOne(Wrappers.lambdaQuery() .eq(KingdeeInvoicePool::getId, id) .last("FOR UPDATE")); - if (invoice == null || Objects.equals(invoice.getIsDeleted(), 1) - || !Objects.equals(invoice.getStatus(), 1)) { - throw new ServiceException("金蝶票据池发票不存在或已失效"); - } - required(invoice.getInvoiceNo(), "发票号码"); + } + + private KingdeeInvoicePool invoiceSnapshot(InvoiceReceiptSaveRequest request) { + KingdeeInvoicePool invoice = new KingdeeInvoicePool(); + invoice.setId(request.getKingdeeInvoicePoolId()); + invoice.setInvoiceNo(request.getInvoiceNo()); + invoice.setInvoiceDate(request.getInvoiceDate()); + invoice.setInvoiceType(request.getInvoiceType()); + invoice.setTaxRate(request.getTaxRate()); + invoice.setInvoiceAmount(request.getInvoiceAmount()); + invoice.setTaxAmount(request.getTaxAmount()); + invoice.setReceiverName(request.getReceiverName()); + invoice.setIssuerName(request.getIssuerName()); + invoice.setBankName(request.getBankName()); + invoice.setBankAccount(request.getBankAccount()); + invoice.setIssuingBank(request.getIssuingBank()); + invoice.setKingdeeBillNo(request.getKingdeeBillNo()); + invoice.setKingdeeStatus(request.getKingdeeStatus()); + return invoice; + } + + private KingdeeInvoicePool invoiceSnapshot(InvoiceReceipt receipt) { + KingdeeInvoicePool invoice = new KingdeeInvoicePool(); + invoice.setId(receipt.getKingdeeInvoicePoolId()); + invoice.setInvoiceNo(receipt.getInvoiceNo()); + invoice.setInvoiceDate(receipt.getInvoiceDate()); + invoice.setInvoiceType(receipt.getInvoiceType()); + invoice.setTaxRate(receipt.getTaxRate()); + invoice.setInvoiceAmount(receipt.getInvoiceAmount()); + invoice.setTaxAmount(receipt.getTaxAmount()); + invoice.setReceiverName(receipt.getReceiverName()); + invoice.setIssuerName(receipt.getIssuerName()); + invoice.setBankName(receipt.getBankName()); + invoice.setBankAccount(receipt.getBankAccount()); + invoice.setIssuingBank(receipt.getIssuingBank()); + invoice.setKingdeeBillNo(receipt.getKingdeeBillNo()); + invoice.setKingdeeStatus(receipt.getKingdeeStatus()); return invoice; } @@ -456,17 +482,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl relations = settlementRelationMapper.selectList( Wrappers.lambdaQuery() @@ -545,8 +561,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl settlementMap.get(item.getFormalSettlementId())) .toList(); assertCompatible(settlements); - assertInvoiceParties(invoice, settlements.get(0)); - BigDecimal allocatedTotal = BigDecimal.ZERO; for (InvoiceReceiptSettlement relation : relations) { FormalSettlement settlement = settlementMap.get(relation.getFormalSettlementId()); BigDecimal allocated = nonNegative(relation.getAllocatedInvoiceAmount(), "分摊发票金额"); @@ -555,10 +569,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl> settlementCandidates(String keyword, Long flowId) { - KingdeeReceiptFlow flow = existing(flowId); + existing(flowId); List settlements = formalSettlementMapper.selectList( Wrappers.lambdaQuery() .eq(FormalSettlement::getSettlementType, RECEIVABLE) @@ -139,9 +139,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl Func.isEmpty(flow.getCounterpartyName()) - || sameName(flow.getCounterpartyName(), settlement.getPayerName())) - .map(settlement -> candidateRow(settlement)) + .map(this::candidateRow) .filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0) .toList(); } @@ -163,7 +161,6 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl settlementMap = lockSettlements(settlementIds); List settlements = settlementIds.stream().map(settlementMap::get).toList(); assertCompatible(settlements); - assertCounterparty(flow, settlements.get(0)); Map previousClaimedMap = new LinkedHashMap<>(); BigDecimal allocatedTotal = BigDecimal.ZERO; @@ -351,16 +348,6 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpllambdaQuery() .eq(ReceiptClaimSettlement::getFormalSettlementId, settlementId) diff --git a/doc/sql/transport/blade_invoice_application_20260821.sql b/doc/sql/transport/blade_invoice_application_20260821.sql index 67aa6a6..dd1c585 100644 --- a/doc/sql/transport/blade_invoice_application_20260821.sql +++ b/doc/sql/transport/blade_invoice_application_20260821.sql @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS `blade_invoice_application` ( `applicant_name` varchar(100) DEFAULT NULL, `undertaking_dept_id` bigint(20) DEFAULT NULL, `undertaking_dept_name` varchar(100) DEFAULT NULL, - `department_emails` varchar(320) DEFAULT NULL, + `department_emails` varchar(320) DEFAULT NULL COMMENT '部门邮箱(多个以分号分隔,最多3个)', `receiver_invoice_info_id` bigint(20) DEFAULT NULL, `taxpayer_no` varchar(20) DEFAULT NULL, `bank_name` varchar(100) DEFAULT NULL, diff --git a/doc/sql/transport/blade_invoice_application_department_emails_20260827.sql b/doc/sql/transport/blade_invoice_application_department_emails_20260827.sql new file mode 100644 index 0000000..ecdc0df --- /dev/null +++ b/doc/sql/transport/blade_invoice_application_department_emails_20260827.sql @@ -0,0 +1,3 @@ +-- 开票申请部门邮箱支持最多3个客商开票信息邮箱,以分号分隔保存。 +ALTER TABLE `blade_invoice_application` + MODIFY COLUMN `department_emails` varchar(320) DEFAULT NULL COMMENT '部门邮箱(多个以分号分隔,最多3个)'; diff --git a/doc/sql/transport/blade_invoice_item.sql b/doc/sql/transport/blade_invoice_item.sql new file mode 100644 index 0000000..1c7fe82 --- /dev/null +++ b/doc/sql/transport/blade_invoice_item.sql @@ -0,0 +1,28 @@ +-- 开票项目 +DROP TABLE IF EXISTS `blade_invoice_item`; +CREATE TABLE `blade_invoice_item` ( + `id` bigint NOT NULL COMMENT '主键', + `short_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '货物或服务简称', + `tax_classification_code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '税收分类编码', + `category_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商品和服务分类名称', + `default_tax_rate` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '默认税率(百分比)', + `create_user` bigint DEFAULT NULL COMMENT '创建人', + `create_dept` bigint DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int DEFAULT 1 COMMENT '状态', + `is_deleted` int DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_invoice_item_code_name` (`tax_classification_code`,`short_name`), + KEY `idx_invoice_item_category` (`category_name`), + KEY `idx_invoice_item_short_name` (`short_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票项目'; + +-- 系统管理(parent_id=1164733399668962201) +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2075449200000000101,1123598815738675203,'invoice_item','开票项目','invoice_item','/base/invoice-item','iconfont icon-shoucang',61,1,0,1,NULL,'',0), +(2075449200000000102,2075449200000000101,'invoice_item_add','新增','invoice_item_add','', '',1,2,1,1,NULL,'',0), +(2075449200000000103,2075449200000000101,'invoice_item_edit','编辑','invoice_item_edit','', '',2,2,2,1,NULL,'',0), +(2075449200000000104,2075449200000000101,'invoice_item_delete','删除','invoice_item_delete','', '',3,2,3,1,NULL,'',0), +(2075449200000000105,2075449200000000101,'invoice_item_view','查看','invoice_item_view','', '',4,2,2,1,NULL,'',0); From 15dc0791f1ebdfde5c5ac6906570f994897fbcb9 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 28 Aug 2026 02:10:44 +0800 Subject: [PATCH 053/114] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A6=96=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FormalSettlementBatchPaymentRequest.java | 23 ++++++++++ .../FormalSettlementController.java | 8 ++++ .../service/IFormalSettlementService.java | 2 + .../service/impl/BillLedgerServiceImpl.java | 2 - .../service/impl/BillPaymentServiceImpl.java | 3 -- .../impl/FormalSettlementServiceImpl.java | 42 +++++++++++++++++-- .../impl/PreSettlementServiceImpl.java | 31 +++++++------- 7 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementBatchPaymentRequest.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementBatchPaymentRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementBatchPaymentRequest.java new file mode 100644 index 0000000..75fad7f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementBatchPaymentRequest.java @@ -0,0 +1,23 @@ +package org.springblade.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** 正式结算批量付款申请请求。 @author Chill */ +@Data +public class FormalSettlementBatchPaymentRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private List items; + private String remark; + + @Data + public static class Item implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private BigDecimal appliedAmount; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index 8604023..b8d2420 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -20,6 +20,7 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; +import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest; import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest; import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; @@ -158,4 +159,11 @@ public class FormalSettlementController extends BladeController { public R applyPayment(@RequestBody FormalSettlementPaymentRequest request) { return R.data(formalSettlementService.applyPayment(request)); } + + @PostMapping("/apply-payments") + @ApiOperationSupport(order = 18) + @Operation(summary = "批量发起尾款付款申请") + public R> applyPayments(@RequestBody FormalSettlementBatchPaymentRequest request) { + return R.data(formalSettlementService.applyPayments(request)); + } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java index abb997c..710291a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -20,6 +20,7 @@ import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; import java.util.List; import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; +import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest; /** * 正式结算单服务 @@ -41,4 +42,5 @@ public interface IFormalSettlementService extends BaseService List detailFees(Long detailId); void adjustDetail(PreSettlementDetailAdjustRequest request); String applyPayment(FormalSettlementPaymentRequest request); + List applyPayments(FormalSettlementBatchPaymentRequest request); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java index e3645c2..64f7119 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java @@ -86,14 +86,12 @@ public class BillLedgerServiceImpl extends BaseServiceImpl availableOptions(String keyword, Long deptId, Long selectedId) { - LocalDate today = LocalDate.now(); return list(Wrappers.lambdaQuery() .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword) .or().like(BillLedger::getIssuerName, keyword) .or().like(BillLedger::getReceiverName, keyword)) .and(wrapper -> wrapper .gt(BillLedger::getAvailableBalance, BigDecimal.ZERO) - .ge(BillLedger::getMaturityDate, today) .or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId))) .orderByAsc(BillLedger::getMaturityDate) .orderByDesc(BillLedger::getCreateTime) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java index 15125ee..156594a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java @@ -255,9 +255,6 @@ public class BillPaymentServiceImpl extends BaseServiceImpl applyPayments(FormalSettlementBatchPaymentRequest request) { + if (request == null || request.getItems() == null || request.getItems().isEmpty()) { + throw new ServiceException("请至少选择一条正式结算单"); + } + List items = request.getItems(); + if (items.stream().anyMatch(item -> item == null || item.getId() == null)) { + throw new ServiceException("付款申请单据不能为空"); + } + if (items.stream().map(FormalSettlementBatchPaymentRequest.Item::getId).distinct().count() != items.size()) { + throw new ServiceException("付款申请单据不能重复"); + } + List settlements = items.stream().map(item -> existing(item.getId())).toList(); + Long contractId = settlements.get(0).getContractId(); + if (items.size() > 1 && (contractId == null + || settlements.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId())))) { + throw new ServiceException("批量付款申请必须选择同一合同的正式结算单"); + } + List paymentNos = new java.util.ArrayList<>(); + for (int index = 0; index < settlements.size(); index++) { + FormalSettlementBatchPaymentRequest.Item item = items.get(index); + paymentNos.add(createPayment(settlements.get(index), item.getAppliedAmount(), request.getRemark())); + } + return paymentNos; + } + + private String createPayment(FormalSettlement settlement, BigDecimal appliedAmount, String remark) { if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许发起付款申请"); if (!"payable".equals(settlement.getSettlementType())) throw new ServiceException("仅应付正式结算单允许发起付款申请"); - BigDecimal amount = positive(request.getAppliedAmount(), "申请付款金额"); + BigDecimal amount = positive(appliedAmount, "申请付款金额"); BigDecimal available = money(settlement.getSettlementAmount()).subtract(money(settlement.getAppliedPaymentAmount())); if (amount.compareTo(available) > 0) throw new ServiceException("申请付款金额不能超过剩余可申请金额" + available); FormalSettlementPayment payment = new FormalSettlementPayment(); @@ -360,7 +392,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl feeItems = parseFeeItems(feeRow.getFeeItemsJson()); boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + BigDecimal componentAmount = feeItems.values().stream().map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); if (!containsFreight) { + BigDecimal freightAmount = money(feeRow.getFreightAmount()); aggregates.merge( summaryKey(feeTypeMap.getOrDefault("运输费", ""), "运输费"), - money(feeRow.getFreightAmount()), + freightAmount, BigDecimal::add); + componentAmount = componentAmount.add(freightAmount); } feeItems.forEach((feeItem, amount) -> aggregates.merge( summaryKey(feeTypeMap.getOrDefault(feeItem, ""), feeItem), money(amount), BigDecimal::add)); + BigDecimal residualAmount = money(money(feeRow.getSettlementAmountTax()) + .subtract(componentAmount)); + if (residualAmount.signum() != 0) { + aggregates.merge( + summaryKey(feeTypeMap.getOrDefault("其他费用", ""), "其他费用"), + residualAmount, + BigDecimal::add); + } } summaryFeeMapper.delete(Wrappers.lambdaQuery() .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId) @@ -885,9 +897,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl Date: Fri, 28 Aug 2026 15:30:12 +0800 Subject: [PATCH 054/114] fix bug --- .../dto/PaymentApplicationSaveRequest.java | 1 + .../entity/PaymentApplicationSettlement.java | 23 ++++ .../pojo/vo/PaymentApplicationVO.java | 2 + .../PaymentApplicationSettlementMapper.java | 10 ++ .../service/IFormalSettlementService.java | 2 + .../impl/FormalSettlementServiceImpl.java | 118 +++++++++++++++++- .../impl/PaymentApplicationServiceImpl.java | 100 ++++++++++++++- .../impl/ReceiptClaimRecordServiceImpl.java | 10 +- .../service/impl/ReceiptFlowServiceImpl.java | 8 +- .../TransportReconciliationServiceImpl.java | 95 ++++++++++++-- .../wrapper/FormalSettlementWrapper.java | 3 +- .../blade_payment_application_20260821.sql | 11 ++ 12 files changed, 353 insertions(+), 30 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationSettlement.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationSettlementMapper.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java index b24df78..bb453e1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java @@ -33,6 +33,7 @@ public class PaymentApplicationSaveRequest implements Serializable { private Long id; private String paymentType; private Long settlementId; + private List settlementIds; private Long preSettlementId; private Long projectId; private String projectName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationSettlement.java new file mode 100644 index 0000000..0f4dd8a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationSettlement.java @@ -0,0 +1,23 @@ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** 付款申请关联正式结算单。 */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_payment_application_settlement") +public class PaymentApplicationSettlement extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long paymentApplicationId; + private Long formalSettlementId; + private String formalSettlementNo; + private BigDecimal settlementAmount; + private BigDecimal appliedAmount; + private BigDecimal paidAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java index cbacc29..6e6c183 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java @@ -25,6 +25,7 @@ import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.entity.PaymentApplicationInvoice; import org.springblade.transport.pojo.entity.PaymentApplicationRecord; +import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; import java.io.Serial; import java.time.LocalDate; @@ -44,4 +45,5 @@ public class PaymentApplicationVO extends PaymentApplication { @TableField(exist = false) private String kingdeeStatusName; @TableField(exist = false) private List invoices; @TableField(exist = false) private List paymentRecords; + @TableField(exist = false) private List settlements; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationSettlementMapper.java new file mode 100644 index 0000000..78e0285 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PaymentApplicationSettlementMapper.java @@ -0,0 +1,10 @@ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; + +/** 付款申请关联正式结算单 Mapper。 */ +@Mapper +public interface PaymentApplicationSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java index 710291a..85016e0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -41,6 +41,8 @@ public interface IFormalSettlementService extends BaseService String syncKingdee(Long id); List detailFees(Long detailId); void adjustDetail(PreSettlementDetailAdjustRequest request); + void refreshPaymentSummary(Long settlementId); + void refreshPaymentSummariesForPreSettlement(Long preSettlementId); String applyPayment(FormalSettlementPaymentRequest request); List applyPayments(FormalSettlementBatchPaymentRequest request); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index f873660..64a8b88 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -30,6 +30,8 @@ import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementMapper; import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper; import org.springblade.transport.mapper.PaymentApplicationMapper; +import org.springblade.transport.mapper.PaymentApplicationSettlementMapper; +import org.springblade.transport.mapper.ReceiptClaimSettlementMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; import org.springblade.transport.mapper.SettlementAdjustmentMapper; @@ -52,6 +54,8 @@ import org.springblade.transport.pojo.entity.PreSettlementDetail; import org.springblade.transport.pojo.entity.PreSettlementDetailFee; import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; +import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.SettlementAdjustment; @@ -114,6 +118,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() + .eq(FormalSettlementSource::getPreSettlementId, preSettlementId) + .eq(FormalSettlementSource::getIsDeleted, 0)).stream() + .map(FormalSettlementSource::getFormalSettlementId).distinct() + .forEach(this::refreshPaymentSummary); } private List listSummaryFees(Long settlementId) { @@ -838,10 +869,91 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, entity.getId())).stream().map(FormalSettlementSource::getPreSettlementNo).collect(Collectors.joining(","))); return vo; } + private PaymentSummary calculatePaymentSummary(FormalSettlement settlement) { + List preSettlementIds = sourceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSource::getFormalSettlementId, settlement.getId()) + .eq(FormalSettlementSource::getIsDeleted, 0)).stream() + .map(FormalSettlementSource::getPreSettlementId).filter(Objects::nonNull).toList(); + LambdaQueryWrapper query = Wrappers.lambdaQuery() + .eq(PaymentApplication::getIsDeleted, 0); + if (preSettlementIds.isEmpty()) { + query.eq(PaymentApplication::getSettlementId, settlement.getId()); + } else { + query.and(wrapper -> wrapper.eq(PaymentApplication::getSettlementId, settlement.getId()) + .or().in(PaymentApplication::getPreSettlementId, preSettlementIds)); + } + List relations = paymentApplicationSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(PaymentApplicationSettlement::getFormalSettlementId, settlement.getId()) + .eq(PaymentApplicationSettlement::getIsDeleted, 0)); + Set relationApplicationIds = relations.stream().map(PaymentApplicationSettlement::getPaymentApplicationId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + if (!relationApplicationIds.isEmpty()) query.notIn(PaymentApplication::getId, relationApplicationIds); + List applications = paymentApplicationMapper.selectList(query); + BigDecimal appliedAmount = applications.stream() + .filter(item -> REVIEWING.equals(item.getApprovalStatus())) + .map(PaymentApplication::getAppliedAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal paidAmount = applications.stream() + .filter(item -> APPROVED.equals(item.getApprovalStatus())) + .map(PaymentApplication::getPaidAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + Map relationStatuses = relationApplicationIds.isEmpty() ? Map.of() : paymentApplicationMapper.selectList( + Wrappers.lambdaQuery().in(PaymentApplication::getId, relationApplicationIds)) + .stream().collect(Collectors.toMap(PaymentApplication::getId, PaymentApplication::getApprovalStatus)); + appliedAmount = appliedAmount.add(relations.stream() + .filter(item -> REVIEWING.equals(relationStatuses.get(item.getPaymentApplicationId()))) + .map(PaymentApplicationSettlement::getAppliedAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add)); + paidAmount = paidAmount.add(relations.stream() + .filter(item -> APPROVED.equals(relationStatuses.get(item.getPaymentApplicationId()))) + .map(PaymentApplicationSettlement::getPaidAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add)); + List legacyPayments = paymentMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementPayment::getFormalSettlementId, settlement.getId()) + .eq(FormalSettlementPayment::getIsDeleted, 0)); + appliedAmount = appliedAmount.add(legacyPayments.stream() + .filter(item -> REVIEWING.equals(item.getBillStatus())) + .map(FormalSettlementPayment::getAppliedAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add)); + paidAmount = paidAmount.add(legacyPayments.stream() + .filter(item -> APPROVED.equals(item.getBillStatus())) + .map(FormalSettlementPayment::getPaidAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add)); + paidAmount = paidAmount.add(sourceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSource::getFormalSettlementId, settlement.getId()) + .eq(FormalSettlementSource::getIsDeleted, 0)).stream() + .map(FormalSettlementSource::getAdvancePaidAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add)); + if ("receivable".equals(settlement.getSettlementType())) { + paidAmount = paidAmount.add(receiptClaimSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getFormalSettlementId, settlement.getId()) + .eq(ReceiptClaimSettlement::getStatus, 1)) + .stream().map(ReceiptClaimSettlement::getAllocatedReceiptAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add)); + } + return new PaymentSummary(money(appliedAmount), money(paidAmount)); + } + + private String paymentStatus(BigDecimal paidAmount, BigDecimal settlementAmount) { + if (paidAmount.compareTo(BigDecimal.ZERO) <= 0) return "unpaid"; + return paidAmount.compareTo(money(settlementAmount)) >= 0 ? "paid" : "partial"; + } + + private record PaymentSummary(BigDecimal appliedAmount, BigDecimal paidAmount) { + } + private SettlementAdjustmentVO toAdjustmentVO(SettlementAdjustment entity, String kingdeeBillNo) { SettlementAdjustmentVO vo = Objects.requireNonNull( BeanUtil.copyProperties(entity, SettlementAdjustmentVO.class)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 842e01a..f8195ac 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -39,6 +39,7 @@ import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper; import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.PaymentApplicationRecordMapper; +import org.springblade.transport.mapper.PaymentApplicationSettlementMapper; import org.springblade.transport.mapper.TemporaryCreditLimitMapper; import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest; import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest; @@ -54,10 +55,12 @@ import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.entity.PaymentApplicationInvoice; import org.springblade.transport.pojo.entity.PaymentApplicationRecord; +import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; import org.springblade.transport.pojo.entity.TemporaryCreditLimit; import org.springblade.transport.pojo.vo.PaymentApplicationVO; import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.service.IPaymentApplicationService; +import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.wrapper.PaymentApplicationWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -84,6 +87,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl selectPage(IPage page, PaymentApplicationVO query) { @@ -122,6 +127,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(org.springblade.transport.pojo.entity.PaymentApplicationRecord::getPaymentApplicationId, id) .orderByDesc(org.springblade.transport.pojo.entity.PaymentApplicationRecord::getCreateTime))); + vo.setSettlements(settlementRelationMapper.selectList(Wrappers.lambdaQuery() + .eq(PaymentApplicationSettlement::getPaymentApplicationId, id) + .eq(PaymentApplicationSettlement::getIsDeleted, 0) + .orderByAsc(PaymentApplicationSettlement::getCreateTime))); return vo; } @@ -191,7 +200,13 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl formalSettlements = resolveFormalSettlements(request, entity); + if (formalSettlements.size() > 1) fillFormalAggregateReference(entity, formalSettlements); fillBillLedger(entity, request); validateQuota(entity); saveOrUpdate(entity); @@ -225,11 +240,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0 ? "matched" : "unmatched"); entity.setPaidAmount(paidAmount); updateById(entity); + saveSettlementRelations(entity, formalSettlements); return entity.getId(); } @Override @Transactional(rollbackFor = Exception.class) - public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); recordMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); removeById(entity); } + public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); recordMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); settlementRelationMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id)); removeById(entity); } @Override @Transactional(rollbackFor = Exception.class) public void submit(PaymentApplicationStatusRequest request) { @@ -244,6 +260,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0)) throw new ServiceException("付款比例必须在0-100之间"); if (request.getAppliedAmount() == null || request.getAppliedAmount().compareTo(BigDecimal.ZERO) < 0) throw new ServiceException("申请付款金额不能小于0"); - if (!"project_advance".equals(request.getPaymentType()) && request.getSettlementId() == null && request.getPreSettlementId() == null) throw new ServiceException("非项目预付必须关联结算单"); + if (!"project_advance".equals(request.getPaymentType()) && request.getSettlementId() == null + && request.getPreSettlementId() == null && Func.isEmpty(request.getSettlementIds())) throw new ServiceException("非项目预付必须关联结算单"); if ("project_advance".equals(request.getPaymentType()) && request.getProjectId() == null) throw new ServiceException("项目预付必须选择所属项目"); if ("progress_advance".equals(request.getPaymentType()) && request.getPreSettlementId() == null) throw new ServiceException("进度预付必须关联预结算单"); - if ("settlement_payment".equals(request.getPaymentType()) && request.getSettlementId() == null) throw new ServiceException("结算付款必须关联正式结算单"); + if ("settlement_payment".equals(request.getPaymentType()) && request.getSettlementId() == null + && Func.isEmpty(request.getSettlementIds())) throw new ServiceException("结算付款必须关联正式结算单"); } private void validateInvoice(PaymentApplicationInvoiceRequest invoice) { @@ -383,7 +404,70 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0 && money(entity.getPayableAmount()).compareTo(BigDecimal.ZERO) > 0) throw new ServiceException("申请付款金额不能超过可付款金额"); + if (!hasMultipleFormalReferences(request) + && money(entity.getAppliedAmount()).compareTo(money(entity.getPayableAmount())) > 0 + && money(entity.getPayableAmount()).compareTo(BigDecimal.ZERO) > 0) throw new ServiceException("申请付款金额不能超过可付款金额"); + } + + private List resolveFormalSettlements(PaymentApplicationSaveRequest request, PaymentApplication entity) { + if (!"settlement_payment".equals(request.getPaymentType())) return List.of(); + List ids = request.getSettlementIds() == null ? List.of() : request.getSettlementIds(); + if (ids.isEmpty() && request.getSettlementId() != null) ids = List.of(request.getSettlementId()); + ids = ids.stream().filter(Objects::nonNull).distinct().toList(); + List settlements = ids.stream().map(id -> formalSettlementMapper.selectById(id)).toList(); + if (settlements.stream().anyMatch(item -> item == null || Objects.equals(item.getIsDeleted(), 1) + || !APPROVED.equals(item.getApprovalStatus()) || !"payable".equals(item.getSettlementType()))) { + throw new ServiceException("只能选择审批通过的应付正式结算单"); + } + Long contractId = settlements.get(0).getContractId(); + if (contractId == null || settlements.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()))) { + throw new ServiceException("多选正式结算单必须属于同一合同"); + } + return settlements; + } + + private boolean hasMultipleFormalReferences(PaymentApplicationSaveRequest request) { + return "settlement_payment".equals(request.getPaymentType()) && request.getSettlementIds() != null + && request.getSettlementIds().stream().filter(Objects::nonNull).distinct().count() > 1; + } + + private void fillFormalAggregateReference(PaymentApplication entity, List settlements) { + BigDecimal settlementAmount = settlements.stream().map(FormalSettlement::getSettlementAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal payableAmount = settlements.stream() + .map(item -> money(item.getSettlementAmount()).subtract(cumulativeAppliedAmount("settlement_payment", item.getId(), entity.getId())).max(BigDecimal.ZERO)) + .reduce(BigDecimal.ZERO, BigDecimal::add); + if (money(entity.getAppliedAmount()).compareTo(payableAmount) > 0) throw new ServiceException("申请付款金额不能超过所选结算单可付款金额合计"); + FormalSettlement first = settlements.get(0); + entity.setSettlementId(first.getId()); + entity.setSettlementNo(settlements.stream().map(FormalSettlement::getFormalSettlementNo).filter(Objects::nonNull).collect(java.util.stream.Collectors.joining("、"))); + entity.setSettlementAmount(settlementAmount); + entity.setPayableAmount(payableAmount); + } + + private void saveSettlementRelations(PaymentApplication entity, List settlements) { + if (settlements.isEmpty()) return; + settlementRelationMapper.delete(Wrappers.lambdaQuery() + .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())); + BigDecimal totalBase = settlements.stream().map(item -> money(item.getSettlementAmount()).subtract( + cumulativeAppliedAmount("settlement_payment", item.getId(), entity.getId())).max(BigDecimal.ZERO)) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal remainingApplied = money(entity.getAppliedAmount()); + BigDecimal remainingPaid = money(entity.getPaidAmount()); + for (int index = 0; index < settlements.size(); index++) { + FormalSettlement settlement = settlements.get(index); + BigDecimal base = money(settlement.getSettlementAmount()).subtract( + cumulativeAppliedAmount("settlement_payment", settlement.getId(), entity.getId())).max(BigDecimal.ZERO); + BigDecimal applied = index == settlements.size() - 1 ? remainingApplied + : money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN); + BigDecimal paid = index == settlements.size() - 1 ? remainingPaid + : money(entity.getPaidAmount()).multiply(applied).divide(money(entity.getAppliedAmount()).max(BigDecimal.ONE), 2, RoundingMode.DOWN); + PaymentApplicationSettlement relation = new PaymentApplicationSettlement(); + relation.setPaymentApplicationId(entity.getId()); relation.setFormalSettlementId(settlement.getId()); + relation.setFormalSettlementNo(settlement.getFormalSettlementNo()); relation.setSettlementAmount(money(settlement.getSettlementAmount())); + relation.setAppliedAmount(applied); relation.setPaidAmount(paid); settlementRelationMapper.insert(relation); + remainingApplied = remainingApplied.subtract(applied); remainingPaid = remainingPaid.subtract(paid); + } } private void validateQuota(PaymentApplication entity) { @@ -546,7 +630,13 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery().likeRight(PaymentApplication::getPaymentNo, prefix)) + 1); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java index 50b550d..bcf456a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java @@ -45,6 +45,7 @@ import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; import org.springblade.transport.pojo.entity.ReceiptFlowRecord; import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO; import org.springblade.transport.service.IReceiptClaimRecordService; +import org.springblade.transport.service.IFormalSettlementService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -71,6 +72,7 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl selectPage(IPage page, ReceiptFlowVO query) { @@ -212,11 +214,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl internals = internalRows(id); + List externals = externalRows(id); + int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).count(); + int externalUnmatched = (int) externals.stream().filter(item -> !MATCHED.equals(item.getMatchStatus())).count(); + int differenceCount = Math.abs(internals.size() - externals.size()) + Math.min(internalUnmatched, externalUnmatched); + BigDecimal differenceQuantity = sumInternalQuantity(internals).subtract(sumExternalQuantity(externals)).abs(); + BigDecimal differenceAmount = sumInternalAmount(internals).subtract(sumExternalAmount(externals)).abs(); + if (differenceCount != 0 || differenceQuantity.compareTo(BigDecimal.ZERO) != 0 + || differenceAmount.compareTo(BigDecimal.ZERO) != 0) { throw new ServiceException("差异单数、差异货量和差异金额必须全部为0才可完成对账"); } + assertAllMatched(bill); bill.setReconciliationStatus(COMPLETED); bill.setCompletedTime(LocalDateTime.now()); updateById(bill); @@ -370,6 +386,7 @@ public class TransportReconciliationServiceImpl BeanUtil.copyProperties(detail, row); row.setId(null); row.setReconciliationId(billId); row.setFormalSettlementDetailId(detail.getId()); row.setSourceDetailId(detail.getSourceDetailId()); row.setLineNo(lineNo); row.setMatchResult(UNMATCHED); row.setUpdateResult("not_updated"); + fillInternalAddresses(row, detail); if (fee != null) { row.setFormalSettlementDetailFeeId(fee.getId()); row.setSourceCargoFeeId(fee.getSourceFeeId()); row.setCargoName(fee.getCargoName()); row.setCargoType(fee.getCargoType()); row.setTransportQuantity(fee.getTransportQuantity()); @@ -383,6 +400,47 @@ public class TransportReconciliationServiceImpl internalMapper.insert(row); } + private void fillInternalAddresses(TransportReconciliationInternal row, FormalSettlementDetail detail) { + ReceivablePayableDetail source = detail.getSourceDetailId() == null ? null + : receivablePayableMapper.selectById(detail.getSourceDetailId()); + if (source != null && MASTER_ORDER_SOURCE.equals(source.getSourceType())) { + MasterOrder masterOrder = masterOrderMapper.selectOne(Wrappers.lambdaQuery() + .eq(MasterOrder::getMasterNo, source.getWaybillNo())); + if (masterOrder != null) { + copyAddresses(row, masterOrder.getDepartureAddress(), masterOrder.getDepartureName(), + masterOrder.getArrivalAddress(), masterOrder.getArrivalName()); + return; + } + } + if (source != null && LOADING_ORDER_SOURCE.equals(source.getSourceType())) { + LoadingManage loading = loadingManageMapper.selectOne(Wrappers.lambdaQuery() + .eq(LoadingManage::getLoadingNo, source.getWaybillNo())); + if (loading != null) { + copyAddresses(row, loading.getDepartureAddress(), null, loading.getArrivalAddress(), null); + return; + } + } + Long waybillId = detail.getWaybillId() != null ? detail.getWaybillId() + : source == null ? null : source.getWaybillId(); + Waybill waybill = waybillId == null ? null : waybillMapper.selectById(waybillId); + if (waybill == null) { + String waybillNo = firstNotEmpty(detail.getWaybillNo(), source == null ? null : source.getWaybillNo()); + if (Func.isNotEmpty(waybillNo)) { + waybill = waybillMapper.selectOne(Wrappers.lambdaQuery().eq(Waybill::getWaybillNo, waybillNo)); + } + } + if (waybill != null) { + copyAddresses(row, waybill.getDepartureAddress(), waybill.getDepartureName(), + waybill.getArrivalAddress(), waybill.getArrivalName()); + } + } + + private void copyAddresses(TransportReconciliationInternal row, String departureAddress, String departureName, + String arrivalAddress, String arrivalName) { + row.setDepartureAddress(firstNotEmpty(departureAddress, departureName, row.getDepartureAddress())); + row.setArrivalAddress(firstNotEmpty(arrivalAddress, arrivalName, row.getArrivalAddress())); + } + private void applyAmount(TransportReconciliation bill, TransportReconciliationInternal internal, TransportReconciliationExternal external) { BigDecimal before = money(internal.getSettlementAmount()); BigDecimal after = money(external.getSettlementAmount()); @@ -395,7 +453,18 @@ public class TransportReconciliationServiceImpl } } else { FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); - detail.setSettlementAmountTax(after); detail.setAdjustAmount(after.subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, internal.getFormalSettlementDetailId())); + if (fees.size() == 1) { + FormalSettlementDetailFee fee = fees.get(0); + fee.setSettlementAmountTax(after); fee.setAdjustAmount(after.subtract(money(fee.getOriginalAmount()))); formalDetailFeeMapper.updateById(fee); + if (fee.getSourceFeeId() != null) { + ReceivablePayableCargoFee sourceFee = cargoFeeMapper.selectById(fee.getSourceFeeId()); + if (sourceFee != null) { sourceFee.setAfterAmount(after); sourceFee.setAdjustAmount(after.subtract(money(sourceFee.getOriginalAmount()))); cargoFeeMapper.updateById(sourceFee); } + } + } else { + detail.setSettlementAmountTax(after); detail.setAdjustAmount(after.subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + } ReceivablePayableDetail source = receivablePayableMapper.selectById(internal.getSourceDetailId()); if (source != null) { source.setTotalAmount(after); receivablePayableMapper.updateById(source); } } @@ -537,10 +606,14 @@ public class TransportReconciliationServiceImpl private LocalDateTime parseTimeNullable(String value, String field) { if (Func.isEmpty(value)) return null; - for (String pattern : List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm")) { + for (String pattern : List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-M-d HH:mm:ss", "yyyy-M-d HH:mm", + "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/M/d HH:mm:ss", "yyyy/M/d HH:mm")) { try { return LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)); } catch (DateTimeParseException ignored) { } } - throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss"); + for (String pattern : List.of("yyyy-MM-dd", "yyyy-M-d", "yyyy/MM/dd", "yyyy/M/d")) { + try { return LocalDate.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)).atStartOfDay(); } catch (DateTimeParseException ignored) { } + } + throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss或yyyy-MM-dd"); } private String vehicleKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), row.getCargoName(), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); } @@ -549,6 +622,10 @@ public class TransportReconciliationServiceImpl private String cargoKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getActualDepartureTime(), row.getTransportQuantity()); } private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); } private String normal(Object value) { if (value == null) return ""; if (value instanceof BigDecimal decimal) return decimal.stripTrailingZeros().toPlainString(); return value.toString().trim().replaceAll("\\s+", "").toLowerCase(); } + private String firstNotEmpty(String... values) { + for (String value : values) if (Func.isNotEmpty(value)) return value; + return null; + } private boolean equalMoney(BigDecimal left, BigDecimal right) { return money(left).compareTo(money(right)) == 0; } private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java index 4f4a331..bb14a73 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java @@ -32,8 +32,9 @@ public class FormalSettlementWrapper extends BaseEntityWrapper Date: Fri, 28 Aug 2026 15:59:35 +0800 Subject: [PATCH 055/114] fix bug --- .../transport/service/impl/BillPaymentServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java index 156594a..180cb49 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillPaymentServiceImpl.java @@ -74,6 +74,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl selectPage(IPage page, BillPaymentVO query) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(query.getBillLedgerId() != null, BillPayment::getBillLedgerId, query.getBillLedgerId()) .like(Func.isNotEmpty(query.getPaymentNo()), BillPayment::getPaymentNo, query.getPaymentNo()) .like(Func.isNotEmpty(query.getDeptName()), BillPayment::getDeptName, query.getDeptName()) .ge(query.getPaymentStartDate() != null, BillPayment::getPaymentDate, query.getPaymentStartDate()) From a029463f82ddc5926ebbd0ecd48d9332679a6646 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 28 Aug 2026 16:58:52 +0800 Subject: [PATCH 056/114] fix bug --- .../transport/service/impl/PreSettlementServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 97c7149..06fafe7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -941,7 +941,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl Date: Fri, 28 Aug 2026 18:05:08 +0800 Subject: [PATCH 057/114] fix bug --- .../controller/LoadingManageController.java | 4 +- .../service/ILoadingManageService.java | 2 +- .../impl/LoadingManageServiceImpl.java | 37 ++++++++++++++----- .../service/impl/MasterOrderServiceImpl.java | 11 ++---- .../service/impl/WaybillServiceImpl.java | 27 ++++++++++++++ 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java index b1d29f9..ff7cacc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java @@ -57,8 +57,8 @@ public class LoadingManageController extends BladeController { @GetMapping("/carrier-contracts") @ApiOperationSupport(order = 13) @Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同") - public R> carrierContracts() { - return R.data(loadingManageService.carrierContracts()); + public R> carrierContracts(@RequestParam List projectIds) { + return R.data(loadingManageService.carrierContracts(projectIds)); } @GetMapping("/list") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index 73f4123..8fd9239 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -25,7 +25,7 @@ public interface ILoadingManageService extends BaseService { LoadingManageVO detail(Long id); - List carrierContracts(); + List carrierContracts(List projectIds); boolean saveDraft(LoadingManage loadingManage); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index b8550c2..0768124 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -85,8 +85,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl carrierContracts() { - return availableCarrierContracts().stream().map(contract -> { + public List carrierContracts(List projectIds) { + if (Func.isEmpty(projectIds)) { + return List.of(); + } + return availableCarrierContracts(projectIds).stream().map(contract -> { LoadingCarrierContractVO option = new LoadingCarrierContractVO(); option.setId(contract.getId()); option.setContractName(contract.getContractName()); @@ -289,7 +292,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); if (Func.isEmpty(waybillIdList)) { return false; @@ -496,7 +499,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl Objects.equals(contract.getId(), loadingManage.getCarrierContractId())) .findFirst() .orElseThrow(() -> new ServiceException("所选承运商合同不存在、未审核通过或已失效")); @@ -599,7 +602,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl availableCarrierContracts() { + private List projectIdsByWaybills(LoadingManage loadingManage) { + List waybillIds = waybillIds(loadingManage.getWaybillIdsJson()); + if (Func.isEmpty(waybillIds)) { + return List.of(); + } + return waybillMapper.selectList(Wrappers.lambdaQuery() + .select(Waybill::getProjectId) + .eq(Waybill::getIsDeleted, 0) + .in(Waybill::getId, waybillIds)) + .stream().map(Waybill::getProjectId).filter(Objects::nonNull).distinct().toList(); + } + + private List availableCarrierContracts(List projectIds) { + if (Func.isEmpty(projectIds)) { + return List.of(); + } return contractManageService.list(Wrappers.lambdaQuery() .eq(ContractManage::getIsDeleted, 0) .eq(ContractManage::getStatus, 1) .eq(ContractManage::getContractCategory, "承运商合同") + .in(ContractManage::getProjectId, projectIds) .in(ContractManage::getApprovalStatus, "approved", "change_approved") .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) .or().ne(ContractManage::getContractStage, "terminated")) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index e2b5196..4dcfa85 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -400,8 +400,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl contractsByCarrier = new LinkedHashMap<>(); - contractManageService.list(new LambdaQueryWrapper() + return contractManageService.list(new LambdaQueryWrapper() .eq(ContractManage::getIsDeleted, 0) .eq(ContractManage::getProjectId, customerContract.getProjectId()) .eq(ContractManage::getContractCategory, "承运商合同") @@ -409,21 +408,19 @@ public class MasterOrderServiceImpl extends BaseServiceImpl wrapper.isNull(ContractManage::getContractStage) .or().ne(ContractManage::getContractStage, "terminated")) .orderByDesc(ContractManage::getCreateTime)) - .stream().filter(contract -> Func.isNotEmpty(contract.getPartyB())) - .forEach(contract -> contractsByCarrier.putIfAbsent(contract.getPartyB(), contract)); - return new ArrayList<>(contractsByCarrier.values()); + .stream().filter(contract -> Func.isNotEmpty(contract.getPartyB())).toList(); } private void validateCarrier(Map dispatch, Map availableCarrierContracts) { String carrierType = string(dispatch, "carrierType", "承运商"); - if ("自运".equals(carrierType)) { + if (!"承运商".equals(carrierType)) { dispatch.remove("carrierContractId"); dispatch.remove("carrierName"); } String carrierName = string(dispatch, "carrierName"); Long carrierContractId = longValue(dispatch, "carrierContractId"); ContractManage carrierContract = carrierContractId == null ? null : availableCarrierContracts.get(carrierContractId); - if (!"自运".equals(carrierType) && (carrierContract == null + if ("承运商".equals(carrierType) && (carrierContract == null || !Objects.equals(carrierContract.getPartyB(), carrierName))) { throw new ServiceException("所选承运商不属于总单客户合同对应项目的有效承运商合同乙方"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 0596201..7b51e3d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -37,6 +37,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.pojo.entity.LoadingManage; +import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ProcessConfig; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; @@ -45,6 +46,7 @@ import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IProcessConfigService; import org.springblade.transport.service.IReceivablePayableDetailService; +import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.wrapper.WaybillWrapper; @@ -77,6 +79,9 @@ public class WaybillServiceImpl extends BaseServiceImpl @jakarta.annotation.Resource private IProcessConfigService processConfigService; + @jakarta.annotation.Resource + private IContractManageService contractManageService; + @jakarta.annotation.Resource @org.springframework.context.annotation.Lazy private IReceivablePayableDetailService receivablePayableDetailService; @@ -593,6 +598,7 @@ public class WaybillServiceImpl extends BaseServiceImpl } } TransportBusinessSupport.validateRequired(waybill.getCarrierJson(), "承运信息不能为空"); + validateCarrierContract(waybill); if (Func.isEmpty(waybill.getQuantity())) { throw new ServiceException("数量不能为空"); } @@ -664,6 +670,23 @@ public class WaybillServiceImpl extends BaseServiceImpl } } + private void validateCarrierContract(Waybill waybill) { + if (!"承运商".equals(waybill.getCarrierType())) { + waybill.setCarrierContractId(null); + return; + } + if (Func.isEmpty(waybill.getCarrierContractId())) { + throw new ServiceException("请选择承运商合同"); + } + ContractManage carrierContract = contractManageService.getById(waybill.getCarrierContractId()); + if (carrierContract == null || Objects.equals(carrierContract.getIsDeleted(), 1) + || !"承运商合同".equals(carrierContract.getContractCategory()) + || !Objects.equals(carrierContract.getProjectId(), waybill.getProjectId()) + || !Objects.equals(carrierContract.getPartyB(), waybill.getCarrierName())) { + throw new ServiceException("承运商合同必须属于所选项目,且合同乙方须与承运商一致"); + } + } + private Waybill loadEditable(Long id, boolean checkDept) { if (Func.isEmpty(id)) { throw new ServiceException("主键不能为空"); @@ -746,6 +769,7 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.getCarrierType(), waybill.getCarrierId(), waybill.getCarrierName(), + waybill.getCarrierContractId(), waybill.getDriverId(), waybill.getDriverName(), waybill.getDriverPhone(), @@ -762,6 +786,7 @@ public class WaybillServiceImpl extends BaseServiceImpl putIfNotEmpty(carrier, "carrierId", waybill.getCarrierId()); putIfNotEmpty(carrier, "carrier", waybill.getCarrierName()); putIfNotEmpty(carrier, "carrierName", waybill.getCarrierName()); + putIfNotEmpty(carrier, "carrierContractId", waybill.getCarrierContractId()); putIfNotEmpty(carrier, "driverId", waybill.getDriverId()); putIfNotEmpty(carrier, "driverName", waybill.getDriverName()); putIfNotEmpty(carrier, "driverPhone", waybill.getDriverPhone()); @@ -783,6 +808,7 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.getCarrierType(), waybill.getCarrierId(), waybill.getCarrierName(), + waybill.getCarrierContractId(), waybill.getDriverId(), waybill.getDriverName(), waybill.getDriverPhone(), @@ -805,6 +831,7 @@ public class WaybillServiceImpl extends BaseServiceImpl putIfNotEmpty(taskInfo, "carrierType", waybill.getCarrierType()); putIfNotEmpty(taskInfo, "carrierId", waybill.getCarrierId()); putIfNotEmpty(taskInfo, "carrierName", waybill.getCarrierName()); + putIfNotEmpty(taskInfo, "carrierContractId", waybill.getCarrierContractId()); putIfNotEmpty(taskInfo, "driverId", waybill.getDriverId()); putIfNotEmpty(taskInfo, "driverName", waybill.getDriverName()); putIfNotEmpty(taskInfo, "driverPhone", waybill.getDriverPhone()); From 12996eb7392818a7f4e043f809489c48205dae57 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 31 Aug 2026 10:38:14 +0800 Subject: [PATCH 058/114] fix bug --- .../pojo/dto/FormalSettlementSaveRequest.java | 9 + .../dto/SettlementAdjustmentSaveRequest.java | 1 + .../pojo/dto/WaybillMileageRequest.java | 56 ++++++ .../entity/ReceivablePayableCargoFee.java | 5 +- .../transport/pojo/entity/Waybill.java | 3 + .../transport/pojo/vo/WaybillVO.java | 4 + .../SettlementAdjustmentController.java | 3 + .../controller/WaybillController.java | 18 +- .../IReceivablePayableDetailService.java | 4 + .../transport/service/IWaybillService.java | 2 + .../impl/FormalSettlementServiceImpl.java | 147 ++++++++++++++- .../impl/InvoiceApplicationServiceImpl.java | 2 +- .../impl/InvoiceReceiptServiceImpl.java | 2 +- .../ReceivablePayableDetailServiceImpl.java | 138 +++++++++++--- .../impl/SettlementAdjustmentServiceImpl.java | 169 ++++++++++++++---- .../service/impl/WaybillServiceImpl.java | 54 +++++- ...yable_cargo_fee_billing_rules_20260830.sql | 28 +++ ...ade_receivable_payable_detail_20260812.sql | 3 +- .../blade_settlement_adjustment_20260818.sql | 6 +- ...ent_adjustment_draft_nullable_20260831.sql | 5 + ...tlement_adjustment_manual_fee_20260830.sql | 5 + doc/sql/transport/blade_tms_business.sql | 2 + .../blade_waybill_mileage_remark_20260831.sql | 8 + 23 files changed, 602 insertions(+), 72 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillMileageRequest.java create mode 100644 doc/sql/transport/blade_receivable_payable_cargo_fee_billing_rules_20260830.sql create mode 100644 doc/sql/transport/blade_settlement_adjustment_draft_nullable_20260831.sql create mode 100644 doc/sql/transport/blade_settlement_adjustment_manual_fee_20260830.sql create mode 100644 doc/sql/transport/blade_waybill_mileage_remark_20260831.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java index 7e5c842..03210df 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -24,6 +24,7 @@ public class FormalSettlementSaveRequest implements Serializable { private String settlementType; private List sourcePreSettlementIds; private List sourceDetailIds; + private List detailAdjustments; private LocalDate exchangeRateDate; private BigDecimal exchangeRate; private String attachmentsJson; @@ -31,6 +32,14 @@ public class FormalSettlementSaveRequest implements Serializable { private List summaryFees; private List invoices; + @Data + public static class DetailAdjustment implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long sourcePreSettlementDetailId; + private Long sourceDetailId; + private BigDecimal adjustAmount; + } + @Data public static class SummaryFee implements Serializable { @Serial private static final long serialVersionUID = 1L; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java index 9aa016c..1b3d27e 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java @@ -22,6 +22,7 @@ public class SettlementAdjustmentSaveRequest implements Serializable { private Long formalSettlementDetailFeeId; private String feeType; private String feeItem; + private BigDecimal originalAmountTax; private BigDecimal adjustmentAmountTax; private BigDecimal adjustmentAmountNoTax; private String remark; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillMileageRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillMileageRequest.java new file mode 100644 index 0000000..fc78b14 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillMileageRequest.java @@ -0,0 +1,56 @@ +/** + * 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.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 运单里程维护请求 + * + * @author Chill + */ +@Data +@Schema(description = "运单里程维护请求") +public class WaybillMileageRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long id; + + @Schema(description = "里程(公里)") + private BigDecimal mileage; + + @Schema(description = "里程维护备注") + private String mileageRemark; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java index a812b0c..8c6acb2 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java @@ -54,7 +54,7 @@ public class ReceivablePayableCargoFee extends TenantEntity { @Schema(description = "行号") private String lineNo; - @Schema(description = "来源:自动生成/手工录入") + @Schema(description = "来源:自动生成/手动录入") private String dataSource; @Schema(description = "货物名称") @@ -75,6 +75,9 @@ public class ReceivablePayableCargoFee extends TenantEntity { @Schema(description = "计费类型") private String billingType; + @Schema(description = "命中计费规则JSON") + private String billingRulesJson; + @Schema(description = "运输量") private BigDecimal transportQuantity; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index e712ce0..aaa207b 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -163,6 +163,9 @@ public class Waybill extends TenantEntity { @Schema(description = "里程") private BigDecimal mileage; + @Schema(description = "里程维护备注") + private String mileageRemark; + @Schema(description = "预计发货日期") private LocalDate estimatedStartTime; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java index 590c804..915d715 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java @@ -51,6 +51,10 @@ public class WaybillVO extends Waybill { @Schema(description = "是否只读") private Boolean readonly; + @TableField(exist = false) + @Schema(description = "是否允许维护里程") + private Boolean mileageMaintainable; + @TableField(exist = false) @Schema(description = "创建人姓名") private String createUserName; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java index 497ed40..a846941 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java @@ -11,6 +11,7 @@ import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest; import org.springblade.transport.pojo.entity.SettlementAdjustment; import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; +import org.springblade.transport.service.IPreSettlementService; import org.springblade.transport.service.ISettlementAdjustmentService; import org.springframework.web.bind.annotation.*; @@ -23,10 +24,12 @@ import java.util.Map; @RequestMapping("/settlement-adjustment") public class SettlementAdjustmentController extends BladeController { private final ISettlementAdjustmentService service; + private final IPreSettlementService preSettlementService; @GetMapping("/list") public R> list(SettlementAdjustmentVO query, Query page) { return R.data(service.selectPage(Condition.getPage(page), query)); } @GetMapping("/detail") public R detail(@RequestParam Long id) { return R.data(service.detail(id)); } @GetMapping("/candidate-formal-settlements") public R>> candidates(@RequestParam(required = false) String keyword) { return R.data(service.candidateFormalSettlements(keyword)); } @GetMapping("/formal-details") public R>> formalDetails(@RequestParam Long formalSettlementId) { return R.data(service.formalDetails(formalSettlementId)); } + @GetMapping("/fee-options") public R>> feeOptions() { return R.data(preSettlementService.feeOptions()); } @PostMapping("/save") public R save(@RequestBody SettlementAdjustmentSaveRequest request) { return R.data(service.saveDraft(request)); } @PostMapping("/remove") public R remove(@RequestParam Long id) { service.removeDraft(id); return R.success("删除成功"); } @PostMapping("/submit") public R submit(@RequestBody SettlementAdjustmentStatusRequest request) { service.submit(request); return R.success("提交成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index c9c8773..a21f338 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -54,6 +54,7 @@ import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillImportBatchVO; import org.springblade.transport.pojo.vo.WaybillVO; +import org.springblade.transport.pojo.dto.WaybillMileageRequest; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.ICustomerArchiveService; import org.springblade.transport.service.IProjectApplyService; @@ -235,36 +236,43 @@ public class WaybillController extends BladeController { return R.status(waybillService.changeRoute(waybill)); } - @PostMapping("/cancel") + @PostMapping("/maintain-mileage") @ApiOperationSupport(order = 17) + @Operation(summary = "维护里程", description = "仅已完成且未生成结算单的运单允许维护") + public R maintainMileage(@RequestBody WaybillMileageRequest request) { + return R.status(waybillService.maintainMileage(request)); + } + + @PostMapping("/cancel") + @ApiOperationSupport(order = 18) @Operation(summary = "取消", description = "传入id") public R cancel(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.cancel(id)); } @PostMapping("/reassign") - @ApiOperationSupport(order = 18) + @ApiOperationSupport(order = 19) @Operation(summary = "重新派单", description = "传入id") public R reassign(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.reassign(id)); } @PostMapping("/complete") - @ApiOperationSupport(order = 19) + @ApiOperationSupport(order = 20) @Operation(summary = "完成", description = "传入id") public R complete(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(waybillService.complete(id)); } @PostMapping("/batch-complete") - @ApiOperationSupport(order = 20) + @ApiOperationSupport(order = 21) @Operation(summary = "批量完成", description = "传入ids") public R batchComplete(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.batchComplete(ids)); } @PostMapping("/road-loading") - @ApiOperationSupport(order = 21) + @ApiOperationSupport(order = 22) @Operation(summary = "公路配载", description = "传入ids") public R roadLoading(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.roadLoading(ids)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index b5150b0..454e311 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -38,6 +38,8 @@ import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; import java.util.List; import java.util.Map; +import java.util.Collection; +import java.util.Set; /** * 应收应付明细服务 @@ -50,6 +52,8 @@ public interface IReceivablePayableDetailService extends BaseService selectList(ReceivablePayableDetailVO query); + Set settlementLinkedWaybillIds(Collection waybillIds); + ReceivablePayableFeeDetailVO feeDetail(Long id); IPage changeRecords(IPage page, Long detailId); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java index c73eb58..791d1fc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java @@ -29,6 +29,7 @@ import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillVO; +import org.springblade.transport.pojo.dto.WaybillMileageRequest; import java.util.List; @@ -47,6 +48,7 @@ public interface IWaybillService extends BaseService { List importWaybill(List data); WaybillVO copy(Long id); boolean changeRoute(Waybill waybill); + boolean maintainMileage(WaybillMileageRequest request); boolean cancel(Long id); boolean reassign(Long id); boolean complete(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 64a8b88..e32d29c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -25,6 +25,8 @@ import org.springblade.transport.mapper.FormalSettlementInvoiceMapper; import org.springblade.transport.mapper.FormalSettlementSourceMapper; import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; +import org.springblade.transport.mapper.InvoiceReceiptMapper; +import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper; import org.springblade.transport.mapper.PreSettlementDetailMapper; import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementMapper; @@ -48,6 +50,8 @@ import org.springblade.transport.pojo.entity.FormalSettlementInvoice; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springblade.transport.pojo.entity.InvoiceReceipt; +import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlementDetail; @@ -80,6 +84,7 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -108,6 +113,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() .eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime))); - vo.setInvoices(invoiceMapper.selectList(Wrappers.lambdaQuery() - .eq(FormalSettlementInvoice::getFormalSettlementId, id).orderByAsc(FormalSettlementInvoice::getLineNo))); + vo.setInvoices(listInvoices(settlement)); List preSettlementIds = sources.stream().map(FormalSettlementSource::getPreSettlementId).toList(); LambdaQueryWrapper paymentApplicationQuery = Wrappers.lambdaQuery(); if (preSettlementIds.isEmpty()) { @@ -225,6 +231,63 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl listInvoices(FormalSettlement settlement) { + List settlementInvoices = invoiceMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementInvoice::getFormalSettlementId, settlement.getId()) + .orderByAsc(FormalSettlementInvoice::getLineNo)); + Map invoiceMap = new LinkedHashMap<>(); + for (FormalSettlementInvoice invoice : settlementInvoices) { + invoiceMap.put(invoiceKey(invoice.getInvoiceNo(), "settlement:" + invoice.getId()), invoice); + } + + List receiptRelations = invoiceReceiptSettlementMapper.selectList( + Wrappers.lambdaQuery() + .and(wrapper -> wrapper + .eq(InvoiceReceiptSettlement::getFormalSettlementId, settlement.getId()) + .or() + .eq(InvoiceReceiptSettlement::getFormalSettlementNo, settlement.getFormalSettlementNo())) + .orderByAsc(InvoiceReceiptSettlement::getCreateTime)); + if (!receiptRelations.isEmpty()) { + List receiptIds = receiptRelations.stream() + .map(InvoiceReceiptSettlement::getInvoiceReceiptId) + .filter(Objects::nonNull) + .distinct() + .toList(); + Map receiptMap = receiptIds.isEmpty() ? Map.of() + : invoiceReceiptMapper.selectByIds(receiptIds).stream() + .filter(receipt -> !VOIDED.equals(receipt.getApprovalStatus()) + && !Objects.equals(receipt.getIsDeleted(), 1)) + .collect(Collectors.toMap(InvoiceReceipt::getId, Function.identity())); + for (InvoiceReceiptSettlement relation : receiptRelations) { + InvoiceReceipt receipt = receiptMap.get(relation.getInvoiceReceiptId()); + if (receipt == null) continue; + FormalSettlementInvoice invoice = new FormalSettlementInvoice(); + invoice.setFormalSettlementId(settlement.getId()); + invoice.setInvoiceNo(receipt.getInvoiceNo()); + invoice.setInvoiceDate(receipt.getInvoiceDate()); + invoice.setInvoiceType(receipt.getInvoiceType()); + invoice.setTaxRate(receipt.getTaxRate()); + invoice.setInvoiceAmount(money(receipt.getInvoiceAmount())); + invoice.setAvailableInvoiceAmount(money(receipt.getInvoiceAmount())); + invoice.setMatchedAmount(money(relation.getAllocatedInvoiceAmount())); + invoice.setAttachmentJson(receipt.getAttachmentsJson()); + invoiceMap.put(invoiceKey(receipt.getInvoiceNo(), "receipt:" + receipt.getId()), invoice); + } + } + + List invoices = new ArrayList<>(invoiceMap.values()); + for (int index = 0; index < invoices.size(); index++) { + invoices.get(index).setLineNo(index + 1); + } + return invoices; + } + + private String invoiceKey(String invoiceNo, String fallback) { + String normalizedInvoiceNo = invoiceNo == null ? "" : invoiceNo.trim(); + return normalizedInvoiceNo.isEmpty() ? fallback : normalizedInvoiceNo; + } + @Override @Transactional(rollbackFor = Exception.class) public Long saveDraft(FormalSettlementSaveRequest request) { @@ -282,6 +345,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl requestRows) { + if (Func.isEmpty(requestRows)) return; + List details = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlementId)); + Map preSettlementDetailMap = details.stream() + .filter(item -> item.getSourcePreSettlementDetailId() != null) + .collect(Collectors.toMap(FormalSettlementDetail::getSourcePreSettlementDetailId, + Function.identity(), (first, second) -> first)); + Map sourceDetailMap = details.stream() + .filter(item -> item.getSourcePreSettlementDetailId() == null && item.getSourceDetailId() != null) + .collect(Collectors.toMap(FormalSettlementDetail::getSourceDetailId, + Function.identity(), (first, second) -> first)); + Set adjustedDetailKeys = new LinkedHashSet<>(); + for (FormalSettlementSaveRequest.DetailAdjustment requestRow : requestRows) { + if (requestRow == null) throw new ServiceException("存在无效的结算明细调整"); + Long preSettlementDetailId = requestRow.getSourcePreSettlementDetailId(); + Long sourceDetailId = requestRow.getSourceDetailId(); + if (preSettlementDetailId != null && sourceDetailId != null) { + throw new ServiceException("结算明细调整只能指定一个来源明细"); + } + String detailKey = preSettlementDetailId != null + ? "pre:" + preSettlementDetailId : sourceDetailId == null ? null : "source:" + sourceDetailId; + if (detailKey == null || !adjustedDetailKeys.add(detailKey)) { + throw new ServiceException("结算明细调整数据无效或重复"); + } + FormalSettlementDetail detail = preSettlementDetailId != null + ? preSettlementDetailMap.get(preSettlementDetailId) : sourceDetailMap.get(sourceDetailId); + if (detail == null) throw new ServiceException("待调整的结算明细不属于当前正式结算单"); + BigDecimal adjustAmount = money(requestRow.getAdjustAmount()); + BigDecimal settlementAmount = money(detail.getOriginalAmount()).add(adjustAmount); + if (settlementAmount.signum() < 0) { + throw new ServiceException("单据" + detail.getDocumentNo() + "调整后的结算金额不能小于0"); + } + applyDetailFeeAmount(detail, settlementAmount); + detail.setAdjustAmount(adjustAmount); + detail.setSettlementAmountTax(settlementAmount); + detailMapper.updateById(detail); + } + } + + private void applyDetailFeeAmount(FormalSettlementDetail detail, BigDecimal settlementAmount) { + List fees = detailFees(detail.getId()); + if (fees.isEmpty()) throw new ServiceException("正式结算明细费用不存在"); + BigDecimal currentAmount = fees.stream().map(FormalSettlementDetailFee::getSettlementAmountTax) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal difference = settlementAmount.subtract(currentAmount); + if (difference.signum() > 0) { + FormalSettlementDetailFee fee = fees.get(0); + fee.setSettlementAmountTax(money(fee.getSettlementAmountTax()).add(difference)); + fee.setAdjustAmount(fee.getSettlementAmountTax().subtract(money(fee.getOriginalAmount()))); + detailFeeMapper.updateById(fee); + return; + } + BigDecimal remainingDeduction = difference.abs(); + for (FormalSettlementDetailFee fee : fees) { + if (remainingDeduction.signum() == 0) break; + BigDecimal currentFeeAmount = money(fee.getSettlementAmountTax()); + BigDecimal deduction = currentFeeAmount.min(remainingDeduction); + fee.setSettlementAmountTax(currentFeeAmount.subtract(deduction)); + fee.setAdjustAmount(fee.getSettlementAmountTax().subtract(money(fee.getOriginalAmount()))); + detailFeeMapper.updateById(fee); + remainingDeduction = remainingDeduction.subtract(deduction); + } + if (remainingDeduction.signum() > 0) { + throw new ServiceException("结算明细调整后的金额无效"); + } + } + private void appendFeeAggregates(Map aggregates, Map feeTypeMap, List detailFees) { for (FormalSettlementDetailFee fee : detailFees) { @@ -648,6 +781,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl= 0 ? "paid" : "partial"; } + private String invoiceStatus(BigDecimal invoiceAmount, BigDecimal settlementAmount) { + BigDecimal matchedAmount = money(invoiceAmount); + if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0) return "unreceived"; + return matchedAmount.compareTo(money(settlementAmount)) == 0 ? "completed" : "partial"; + } + private record PaymentSummary(BigDecimal appliedAmount, BigDecimal paidAmount) { } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index ba1724f..eff9419 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -521,7 +521,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl= 0 ? "completed" : "partial"; + : allocated.compareTo(money(settlement.getSettlementAmount())) == 0 ? "completed" : "partial"; settlement.setInvoiceAmount(allocated); settlement.setInvoiceStatus(status); formalSettlementMapper.updateById(settlement); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java index f6b71bb..2d254ea 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java @@ -500,7 +500,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl= 0 ? "completed" : "partial"; + : invoiceAmount.compareTo(money(settlement.getSettlementAmount())) == 0 ? "completed" : "partial"; settlement.setInvoiceAmount(invoiceAmount); settlement.setInvoiceStatus(invoiceStatus); formalSettlementMapper.updateById(settlement); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 3a91d01..09b27ce 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -103,7 +103,17 @@ public class ReceivablePayableDetailServiceImpl private static final String SOURCE_MASTER_ORDER = "总单系统生成"; private static final String SOURCE_LOADING_ORDER = "配载单系统生成"; private static final String FEE_SOURCE_AUTO = "自动生成"; - private static final String FEE_SOURCE_MANUAL = "手工录入"; + private static final String FEE_SOURCE_MANUAL = "手动录入"; + private static final Set FEE_SOURCE_MANUAL_LEGACY = Set.of("手工录入", "手动添加"); + private static final Map> MANUAL_BILLING_TYPES = Map.of( + "按重量", List.of("固定单价", "区间单价", "阶梯单价", "区间阶梯一口价"), + "按体积", List.of("固定单价", "区间单价", "阶梯单价", "区间阶梯一口价"), + "按车辆", List.of("固定单价"), + "按里程", List.of("固定单价", "区间单价", "阶梯单价", "区间阶梯一口价"), + "按吨·公里", List.of("固定单价", "区间单价", "阶梯单价", "区间阶梯一口价"), + "固定金额(整单一口价)", List.of("固定一口价"), + "按数量", List.of("固定单价", "区间单价", "阶梯单价", "区间阶梯一口价") + ); private final ReceivablePayableCargoFeeMapper cargoFeeMapper; private final ReceivablePayableChangeRecordMapper changeRecordMapper; @@ -148,6 +158,44 @@ public class ReceivablePayableDetailServiceImpl return result; } + @Override + public Set settlementLinkedWaybillIds(Collection waybillIds) { + Set candidateIds = waybillIds == null ? Set.of() : waybillIds.stream() + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (candidateIds.isEmpty()) { + return Set.of(); + } + Set result = list(settlementLinkedQuery() + .in(ReceivablePayableDetail::getWaybillId, candidateIds)) + .stream() + .map(ReceivablePayableDetail::getWaybillId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + List cargoFees = cargoFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .in(ReceivablePayableCargoFee::getWaybillId, candidateIds)); + Set cargoDetailIds = cargoFees.stream() + .map(ReceivablePayableCargoFee::getDetailId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (cargoDetailIds.isEmpty()) { + return result; + } + Set settlementLinkedDetailIds = list(settlementLinkedQuery() + .in(ReceivablePayableDetail::getId, cargoDetailIds)) + .stream() + .map(ReceivablePayableDetail::getId) + .collect(Collectors.toSet()); + cargoFees.stream() + .filter(item -> settlementLinkedDetailIds.contains(item.getDetailId())) + .map(ReceivablePayableCargoFee::getWaybillId) + .filter(Objects::nonNull) + .forEach(result::add); + return result; + } + @Override public ReceivablePayableFeeDetailVO feeDetail(Long id) { ReceivablePayableDetail detail = getExisting(id); @@ -282,11 +330,11 @@ public class ReceivablePayableDetailServiceImpl throw new ServiceException("变更原因不能超过300个字"); } if (Boolean.TRUE.equals(adjusted.getManualFee())) { - if (existing == null && !"receivable".equals(detail.getSettlementType())) { - throw new ServiceException("仅应收明细允许新增手工费用"); + if (existing == null && !"payable".equals(detail.getSettlementType())) { + throw new ServiceException("仅应付明细允许新增费用"); } if (existing != null && !isManualFee(existing)) { - throw new ServiceException("自动生成费用行不能变更为手工录入"); + throw new ServiceException("自动生成费用行不能变更为手动录入"); } validateAdjustRow(adjusted, true); Map manualItems = validatedFeeItems(adjusted.getFeeItems(), allowedFeeItems); @@ -304,8 +352,6 @@ public class ReceivablePayableDetailServiceImpl BigDecimal oldAmount = money(existing.getAfterAmount()); applyEditableFields(existing, adjusted, true); existing.setDataSource(FEE_SOURCE_MANUAL); - existing.setBillingFactor("-"); - existing.setBillingType("-"); existing.setFreightAmount(freightAmount); existing.setFeeItemsJson(JsonUtil.toJson(manualItems)); existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount()))); @@ -318,7 +364,7 @@ public class ReceivablePayableDetailServiceImpl } else { cargoFeeMapper.updateById(existing); } - changes.add("【手工费用】从[" + Objects.toString(oldCargoName, "") + " " + changes.add("【手动录入】从[" + Objects.toString(oldCargoName, "") + " " + formatValue(oldAmount) + "]调整为[" + Objects.toString(existing.getCargoName(), "") + " " + formatValue(afterAmount) + "]"); continue; @@ -327,7 +373,7 @@ public class ReceivablePayableDetailServiceImpl throw new ServiceException("存在无效的费用调整行"); } if (isManualFee(existing)) { - throw new ServiceException("手工录入费用行不能变更为自动生成"); + throw new ServiceException("手动录入费用行不能变更为自动生成"); } validateAdjustRow(adjusted, false); Map feeItems = validatedFeeItems(adjusted.getFeeItems(), allowedFeeItems); @@ -335,6 +381,14 @@ public class ReceivablePayableDetailServiceImpl BigDecimal mileage = money(adjusted.getMileage()); BigDecimal freightAmount = money(adjusted.getFreightAmount()); Map effectiveFeeItems = feeItems; + boolean billingBasisChanged = money(existing.getTransportQuantity()).compareTo(transportQuantity) != 0 + || money(existing.getMileage()).compareTo(mileage) != 0; + if (billingBasisChanged) { + AdjustedFeeCalculation calculation = calculateAdjustedFee(detail, existing, + transportQuantity, mileage, freightAmount, feeItems); + freightAmount = calculation.freightAmount(); + effectiveFeeItems = calculation.feeItems(); + } appendChange(changes, "规格", existing.getSpecification(), adjusted.getSpecification()); appendChange(changes, "型号", existing.getModel(), adjusted.getModel()); appendChange(changes, "计费要素", existing.getBillingFactor(), adjusted.getBillingFactor()); @@ -983,6 +1037,17 @@ public class ReceivablePayableDetailServiceImpl return wrapper.orderByDesc(ReceivablePayableDetail::getCreateTime); } + private LambdaQueryWrapper settlementLinkedQuery() { + return Wrappers.lambdaQuery() + .eq(ReceivablePayableDetail::getIsDeleted, 0) + .and(wrapper -> wrapper + .isNotNull(ReceivablePayableDetail::getPreSettlementNo) + .ne(ReceivablePayableDetail::getPreSettlementNo, "") + .or() + .isNotNull(ReceivablePayableDetail::getFormalSettlementNo) + .ne(ReceivablePayableDetail::getFormalSettlementNo, "")); + } + private LambdaQueryWrapper buildUpdateQuery(ReceivablePayableUpdateFeeRequest request) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(ReceivablePayableDetail::getIsDeleted, 0) @@ -1276,6 +1341,7 @@ public class ReceivablePayableDetailServiceImpl } Map, ReceivablePayableCargoFee> feesByCargo = new LinkedHashMap<>(); Map, Map> feeItemsByCargo = new LinkedHashMap<>(); + Map, List>> billingRulesByCargo = new LinkedHashMap<>(); Set> freightBillingCargoKeys = new LinkedHashSet<>(); for (Object value : (List) plan.get("rules")) { if (!(value instanceof Map raw)) continue; @@ -1292,8 +1358,12 @@ public class ReceivablePayableDetailServiceImpl key -> buildCalculatedCargoFee(waybill, feeWaybill, feeGoods, rule)); Map feeItems = feeItemsByCargo.computeIfAbsent(cargoKey, key -> new LinkedHashMap<>()); + List> billingRules = billingRulesByCargo.computeIfAbsent(cargoKey, + key -> new ArrayList<>()); + billingRules.add(new LinkedHashMap<>(rule)); feeItems.merge(feeItem, amount, BigDecimal::add); fee.setFeeItemsJson(JsonUtil.toJson(feeItems)); + fee.setBillingRulesJson(JsonUtil.toJson(billingRules)); fee.setOriginalAmount(money(fee.getOriginalAmount()).add(amount)); fee.setAfterAmount(fee.getOriginalAmount()); if (isFreightRule(rule)) { @@ -1648,13 +1718,25 @@ public class ReceivablePayableDetailServiceImpl } private boolean isManualFee(ReceivablePayableCargoFee fee) { - return FEE_SOURCE_MANUAL.equals(fee.getDataSource()) || "手工调整".equals(fee.getBillingFactor()); + return FEE_SOURCE_MANUAL.equals(fee.getDataSource()) + || FEE_SOURCE_MANUAL_LEGACY.contains(fee.getDataSource()) + || "手工调整".equals(fee.getBillingFactor()); } private void validateAdjustRow(ReceivablePayableAdjustFeeRequest.AdjustRow row, boolean manualFee) { if (manualFee) { + if (isBlank(row.getCargoName())) { + throw new ServiceException("货物名称不能为空"); + } validateLength(row.getCargoName(), 100, "货物名称"); validateLength(row.getCargoType(), 100, "货物类型"); + List billingTypes = MANUAL_BILLING_TYPES.get(row.getBillingFactor()); + if (billingTypes == null) { + throw new ServiceException("请选择计费要素"); + } + if (!billingTypes.contains(row.getBillingType())) { + throw new ServiceException("请选择计费要素对应的计费类型"); + } } validateLength(row.getSpecification(), 255, "规格"); validateLength(row.getModel(), 255, "型号"); @@ -1688,10 +1770,10 @@ public class ReceivablePayableDetailServiceImpl } private void applyEditableFields(ReceivablePayableCargoFee fee, - ReceivablePayableAdjustFeeRequest.AdjustRow adjusted, - boolean manualFee) { + ReceivablePayableAdjustFeeRequest.AdjustRow adjusted, + boolean manualFee) { if (manualFee) { - fee.setCargoName(adjusted.getCargoName()); + fee.setCargoName(adjusted.getCargoName().trim()); fee.setCargoType(adjusted.getCargoType()); } fee.setSpecification(adjusted.getSpecification()); @@ -1727,12 +1809,18 @@ public class ReceivablePayableDetailServiceImpl if (isManualFee(fee)) { throw new ServiceException("手工费用不支持按合同计费规则试算"); } - ContractManage contract = contractManageService.getById(detail.getContractId()); - if (contract == null) { - throw new ServiceException("关联合同不存在"); - } Waybill adjustedWaybill = adjustedWaybill(detail, fee, transportQuantity, mileage); - List> rules = matchingAdjustedRules(contract, fee, adjustedWaybill); + List> rules = parseList(fee.getBillingRulesJson()); + if (rules.isEmpty()) { + ContractManage contract = contractManageService.getById(detail.getContractId()); + if (contract == null) { + throw new ServiceException("关联合同不存在"); + } + rules = matchingAdjustedRules(contract, fee, adjustedWaybill); + if (!rules.isEmpty()) { + fee.setBillingRulesJson(JsonUtil.toJson(rules)); + } + } if (rules.isEmpty()) { throw new ServiceException("未找到费用明细对应的合同计费规则,请先更新费用"); } @@ -1760,7 +1848,8 @@ public class ReceivablePayableDetailServiceImpl private Waybill adjustedWaybill(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee, BigDecimal transportQuantity, BigDecimal mileage) { - Waybill source = detail.getWaybillId() == null ? null : waybillService.getById(detail.getWaybillId()); + Long waybillId = fee.getWaybillId() == null ? detail.getWaybillId() : fee.getWaybillId(); + Waybill source = waybillId == null ? null : waybillService.getById(waybillId); Waybill waybill = source == null ? new Waybill() : Objects.requireNonNull(BeanUtil.copyProperties(source, Waybill.class)); waybill.setQuantity(transportQuantity); @@ -1791,15 +1880,23 @@ public class ReceivablePayableDetailServiceImpl List> firstCandidates = List.of(); List> defaultCandidates = List.of(); List> billingMatchedCandidates = List.of(); + List> billingFieldCandidates = List.of(); for (Map plan : parseList(contract.getBillingPlanJson())) { if (!(plan.get("rules") instanceof List rules)) continue; List> candidates = new ArrayList<>(); + List> feeItemCandidates = new ArrayList<>(); for (Object value : rules) { if (!(value instanceof Map raw)) continue; Map rule = new LinkedHashMap<>(); raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); - if (!feeItemNames.contains(stringValue(rule, "feeItem")) || !matchesRule(rule, waybill)) continue; - candidates.add(rule); + if (!feeItemNames.contains(stringValue(rule, "feeItem"))) continue; + feeItemCandidates.add(rule); + if (matchesRule(rule, waybill)) candidates.add(rule); + } + if (feeItemCandidates.stream().anyMatch(rule -> matchesBillingFields(rule, fee))) { + if (billingFieldCandidates.isEmpty() || isDefaultPlan(plan)) { + billingFieldCandidates = feeItemCandidates; + } } if (candidates.isEmpty()) continue; if (firstCandidates.isEmpty()) firstCandidates = candidates; @@ -1811,6 +1908,7 @@ public class ReceivablePayableDetailServiceImpl } } if (!billingMatchedCandidates.isEmpty()) return billingMatchedCandidates; + if (!billingFieldCandidates.isEmpty()) return billingFieldCandidates; return defaultCandidates.isEmpty() ? firstCandidates : defaultCandidates; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java index 6970ebd..d45f106 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java @@ -13,6 +13,7 @@ import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementChangeRecordMapper; import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper; import org.springblade.transport.mapper.SettlementAdjustmentDetailMapper; import org.springblade.transport.mapper.SettlementAdjustmentMapper; import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; @@ -21,6 +22,7 @@ import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementChangeRecord; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springblade.transport.pojo.entity.SettlementAdjustment; import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail; import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; @@ -46,10 +48,12 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl> candidateFormalSettlements(String keyword) { List rows = formalMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlement::getApprovalStatus, APPROVED) + .and(wrapper -> wrapper.isNull(FormalSettlement::getPaymentStatus) + .or().ne(FormalSettlement::getPaymentStatus, PAID)) .like(Func.isNotEmpty(keyword), FormalSettlement::getFormalSettlementNo, keyword) .orderByDesc(FormalSettlement::getCreateTime)); List> result = new ArrayList<>(); @@ -104,8 +110,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl> formalDetails(Long formalSettlementId) { - FormalSettlement settlement = formalMapper.selectById(formalSettlementId); - if (settlement == null || !APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单可调整"); + FormalSettlement settlement = adjustableFormalSettlement(formalSettlementId); List> result = new ArrayList<>(); List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, formalSettlementId).orderByAsc(FormalSettlementDetail::getLineNo)); @@ -126,38 +131,64 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpllambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId())); - if (adjustment.getId() == null) { adjustment.setAdjustmentNo(nextNo()); adjustment.setApprovalStatus(DRAFT); } - adjustment.setFormalSettlementId(formal.getId()); adjustment.setFormalSettlementNo(formal.getFormalSettlementNo()); - adjustment.setSettlementType(formal.getSettlementType()); adjustment.setProjectName(formal.getProjectName()); - adjustment.setDeptName(formal.getDeptName()); adjustment.setCustomerName("receivable".equals(formal.getSettlementType()) ? formal.getPayerName() : formal.getPayeeName()); - adjustment.setContractNo(formal.getContractNo()); adjustment.setContractName(formal.getContractName()); - adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); - adjustment.setOriginalSettlementAmount(money(formal.getSettlementAmount())); adjustment.setRemark(limit(request.getRemark(), 200)); + FormalSettlement formal = request.getFormalSettlementId() == null + ? null : formalMapper.selectById(request.getFormalSettlementId()); + if (adjustment.getId() == null) adjustment.setAdjustmentNo(nextNo()); + adjustment.setApprovalStatus(DRAFT); + adjustment.setCurrentNode("草稿"); + adjustment.setCurrentProcessor(AuthUtil.getUserName()); + adjustment.setApprovedTime(null); + applyFormalSnapshot(adjustment, formal, request.getFormalSettlementId()); + adjustment.setRemark(request.getRemark()); adjustment.setAttachmentsJson(request.getAttachmentsJson()); + adjustment.setAdjustmentAmount(BigDecimal.ZERO.setScale(2)); + adjustment.setAdjustedSettlementAmount(money(adjustment.getOriginalSettlementAmount())); + saveOrUpdate(adjustment); + detailMapper.delete(Wrappers.lambdaQuery() + .eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId())); BigDecimal total = BigDecimal.ZERO; if (request.getDetails() != null) for (SettlementAdjustmentSaveRequest.Detail row : request.getDetails()) { - FormalSettlementDetailFee fee = validateFee(formal.getId(), row.getFormalSettlementDetailId(), row.getFormalSettlementDetailFeeId()); - if (adjustment.getId() == null) save(adjustment); + if (row == null) continue; SettlementAdjustmentDetail detail = new SettlementAdjustmentDetail(); detail.setAdjustmentId(adjustment.getId()); detail.setFormalSettlementDetailId(row.getFormalSettlementDetailId()); detail.setFormalSettlementDetailFeeId(row.getFormalSettlementDetailFeeId()); - detail.setFeeType(row.getFeeType()); detail.setFeeItem(row.getFeeItem()); detail.setOriginalAmountTax(fee.getSettlementAmountTax()); - detail.setAdjustmentAmountTax(row.getAdjustmentAmountTax() == null ? BigDecimal.ZERO : row.getAdjustmentAmountTax()); - detail.setAdjustmentAmountNoTax(row.getAdjustmentAmountNoTax()); detail.setRemark(limit(row.getRemark(), 200)); detailMapper.insert(detail); + detail.setFeeType(row.getFeeType()); detail.setFeeItem(row.getFeeItem()); + detail.setOriginalAmountTax(money(row.getOriginalAmountTax())); + detail.setAdjustmentAmountTax(money(row.getAdjustmentAmountTax())); + detail.setAdjustmentAmountNoTax(row.getAdjustmentAmountNoTax()); detail.setRemark(row.getRemark()); detailMapper.insert(detail); total = total.add(detail.getAdjustmentAmountTax()); } - adjustment.setAdjustmentAmount(total); adjustment.setAdjustedSettlementAmount(adjustment.getOriginalSettlementAmount().add(total)); saveOrUpdate(adjustment); + adjustment.setAdjustmentAmount(total); adjustment.setAdjustedSettlementAmount(money(adjustment.getOriginalSettlementAmount()).add(total)); updateById(adjustment); return adjustment.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void removeDraft(Long id) { SettlementAdjustment item = editable(id); detailMapper.delete(Wrappers.lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, id)); removeById(item.getId()); } - @Override public void submit(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), DRAFT, REVIEWING, "审批中", null); } + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(SettlementAdjustmentStatusRequest request) { + SettlementAdjustment item = existing(request.getId()); + if (!DRAFT.equals(item.getApprovalStatus())) throw new ServiceException("仅草稿状态的调整单允许提交"); + FormalSettlement formal = adjustableFormalSettlement(item.getFormalSettlementId()); + List details = detailMapper.selectList( + Wrappers.lambdaQuery() + .eq(SettlementAdjustmentDetail::getAdjustmentId, item.getId())); + if (details.isEmpty()) throw new ServiceException("请至少添加一条调整费用"); + for (SettlementAdjustmentDetail detail : details) { + boolean manualFee = isManualFee(detail); + if (!manualFee && (detail.getFormalSettlementDetailId() == null + || detail.getFormalSettlementDetailFeeId() == null)) { + throw new ServiceException("费用明细关联信息不完整"); + } + if (!manualFee) validateFee(formal.getId(), detail.getFormalSettlementDetailId(), + detail.getFormalSettlementDetailFeeId()); + requiredText(detail.getFeeType(), "费用类型", 100); + requiredText(detail.getFeeItem(), "费用项目", 100); + limit(detail.getRemark(), 200); + } + limit(item.getRemark(), 200); + changeStatus(item.getId(), DRAFT, REVIEWING, "审批中", null); + } @Override public void returnBill(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), REVIEWING, RETURNED, "已驳回", limit(request.getReason(), 200)); } @Override @@ -177,19 +208,26 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpllambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId()))) { - FormalSettlementDetailFee fee = validateFee(formal.getId(), item.getFormalSettlementDetailId(), item.getFormalSettlementDetailFeeId()); - BigDecimal beforeAmount = money(fee.getSettlementAmountTax()); - fee.setSettlementAmountTax(money(fee.getSettlementAmountTax()).add(money(item.getAdjustmentAmountTax()))); - if (item.getAdjustmentAmountNoTax() != null) fee.setSettlementAmountNoTax(money(fee.getSettlementAmountNoTax()).add(item.getAdjustmentAmountNoTax())); - fee.setAdjustAmount(money(fee.getAdjustAmount()).add(item.getAdjustmentAmountTax())); formalFeeMapper.updateById(fee); - FormalSettlementDetail formalDetail = formalDetailMapper.selectById(item.getFormalSettlementDetailId()); - saveFormalChange(formal.getId(), "结算明细项", formalDetail == null ? null : formalDetail.getLineNo(), - "调整", "【" + (Func.isEmpty(item.getFeeItem()) ? "结算费用" : item.getFeeItem()) - + "】从【" + beforeAmount + "】调整为【" + fee.getSettlementAmountTax() + "】", - Func.isEmpty(item.getRemark()) ? adjustment.getRemark() : item.getRemark()); + if (money(item.getAdjustmentAmountTax()).signum() == 0) continue; + if (isManualFee(item)) { + saveFormalChange(formal.getId(), "合计费用项", null, "调整", + "新增【" + item.getFeeItem() + "】调整费用【" + money(item.getAdjustmentAmountTax()) + "】", + Func.isEmpty(item.getRemark()) ? adjustment.getRemark() : item.getRemark()); + } else { + FormalSettlementDetailFee fee = validateFee(formal.getId(), item.getFormalSettlementDetailId(), item.getFormalSettlementDetailFeeId()); + BigDecimal beforeAmount = money(fee.getSettlementAmountTax()); + fee.setSettlementAmountTax(beforeAmount.add(money(item.getAdjustmentAmountTax()))); + if (item.getAdjustmentAmountNoTax() != null) fee.setSettlementAmountNoTax(money(fee.getSettlementAmountNoTax()).add(item.getAdjustmentAmountNoTax())); + fee.setAdjustAmount(money(fee.getAdjustAmount()).add(item.getAdjustmentAmountTax())); formalFeeMapper.updateById(fee); + FormalSettlementDetail formalDetail = formalDetailMapper.selectById(item.getFormalSettlementDetailId()); + saveFormalChange(formal.getId(), "结算明细项", formalDetail == null ? null : formalDetail.getLineNo(), + "调整", "【" + (Func.isEmpty(item.getFeeItem()) ? "结算费用" : item.getFeeItem()) + + "】从【" + beforeAmount + "】调整为【" + fee.getSettlementAmountTax() + "】", + Func.isEmpty(item.getRemark()) ? adjustment.getRemark() : item.getRemark()); + } + applySummaryAdjustment(formal.getId(), item); } for (FormalSettlementDetail detail : formalDetailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()))) { List fees = formalFeeMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())); @@ -197,8 +235,12 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpllambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId())).stream().map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); - formal.setSettlementAmount(amount); formal.setLocalSettlementAmount(amount.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate())); formalMapper.updateById(formal); + BigDecimal amount = money(formal.getSettlementAmount()).add(money(adjustment.getAdjustmentAmount())); + formal.setSettlementAmount(amount); + formal.setLocalSettlementAmount(amount.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate())); + formal.setRemainingPayableAmount(amount.subtract(money(formal.getAppliedPaymentAmount())) + .subtract(money(formal.getPaidAmount())).max(BigDecimal.ZERO)); + formalMapper.updateById(formal); saveFormalChange(formal.getId(), "合计费用项", null, "调整", "调整单" + adjustment.getAdjustmentNo() + "调整金额【" + money(adjustment.getAdjustmentAmount()) + "】", adjustment.getRemark()); @@ -222,11 +264,66 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl formalSummaryFees(Long formalId) { return formalSummaryFeeMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementSummaryFee::getFormalSettlementId, formalId).eq(FormalSettlementSummaryFee::getIsDeleted, 0).orderByAsc(FormalSettlementSummaryFee::getLineNo)); } + private void applySummaryAdjustment(Long formalId, SettlementAdjustmentDetail detail) { + List summaryFees = formalSummaryFees(formalId); + FormalSettlementSummaryFee summaryFee = summaryFees.stream() + .filter(item -> Objects.equals(item.getFeeType(), detail.getFeeType()) && Objects.equals(item.getFeeItem(), detail.getFeeItem())) + .findFirst().orElse(null); + BigDecimal adjustmentAmount = money(detail.getAdjustmentAmountTax()); + if (summaryFee == null) { + summaryFee = new FormalSettlementSummaryFee(); + summaryFee.setFormalSettlementId(formalId); + summaryFee.setLineNo(summaryFees.stream().map(FormalSettlementSummaryFee::getLineNo) + .filter(Objects::nonNull).max(Integer::compareTo).orElse(0) + 1); + summaryFee.setFeeType(detail.getFeeType()); + summaryFee.setFeeItem(detail.getFeeItem()); + summaryFee.setOriginalAmount(BigDecimal.ZERO.setScale(2)); + summaryFee.setAdjustAmount(adjustmentAmount); + summaryFee.setSettlementAmount(adjustmentAmount); + summaryFee.setRemark(""); + summaryFee.setManualFlag(1); + formalSummaryFeeMapper.insert(summaryFee); + return; + } + summaryFee.setAdjustAmount(money(summaryFee.getAdjustAmount()).add(adjustmentAmount)); + summaryFee.setSettlementAmount(money(summaryFee.getSettlementAmount()).add(adjustmentAmount)); + formalSummaryFeeMapper.updateById(summaryFee); + } private SettlementAdjustmentVO toVO(SettlementAdjustment item) { SettlementAdjustmentVO vo = new SettlementAdjustmentVO(); org.springframework.beans.BeanUtils.copyProperties(item, vo); vo.setCreateUserName(UserCache.getUserRealName(item.getCreateUser())); vo.setApprovalStatusName(statusName(item.getApprovalStatus())); vo.setSettlementTypeName(typeName(item.getSettlementType())); return vo; } private String statusName(String value) { return Map.of(DRAFT, "草稿", REVIEWING, "审批中", APPROVED, "审批通过", RETURNED, "已驳回").getOrDefault(value, value); } - private String typeName(String value) { return "receivable".equals(value) ? "应收" : "应付"; } + private String typeName(String value) { return "receivable".equals(value) ? "应收" : "payable".equals(value) ? "应付" : ""; } private synchronized String nextNo() { String prefix = "TZ" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = count(Wrappers.lambdaQuery().likeRight(SettlementAdjustment::getAdjustmentNo, prefix)); return prefix + String.format("%04d", count + 1); } private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private String requiredText(String value, String field, int max) { if (Func.isEmpty(value)) throw new ServiceException(field + "不能为空"); return limit(value.trim(), max); } private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("内容不能超过" + max + "个字"); return value; } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 7b51e3d..ce1e61e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -40,6 +40,7 @@ import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ProcessConfig; import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.dto.WaybillMileageRequest; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillVO; @@ -60,6 +61,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; /** @@ -89,12 +91,16 @@ public class WaybillServiceImpl extends BaseServiceImpl @Override public IPage selectWaybillPage(IPage page, WaybillVO waybill) { IPage entityPage = page(page, buildQuery(waybill)); - return WaybillWrapper.build().pageVO(entityPage); + IPage result = WaybillWrapper.build().pageVO(entityPage); + fillMileageMaintainable(result.getRecords()); + return result; } @Override public WaybillVO detail(Long id) { - return WaybillWrapper.build().entityVO(loadEditable(id, false)); + WaybillVO result = WaybillWrapper.build().entityVO(loadEditable(id, false)); + fillMileageMaintainable(List.of(result)); + return result; } @Override @@ -108,8 +114,10 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setLoadingNo(oldRecord.getLoadingNo()); waybill.setDeptId(oldRecord.getDeptId()); waybill.setDeptName(oldRecord.getDeptName()); + waybill.setMileageRemark(oldRecord.getMileageRemark()); } else { waybill.setLoadingNo(null); + waybill.setMileageRemark(null); fillProjectProcessConfig(waybill); } prepare(waybill); @@ -306,6 +314,32 @@ public class WaybillServiceImpl extends BaseServiceImpl return updateById(oldRecord); } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean maintainMileage(WaybillMileageRequest request) { + if (request == null || request.getId() == null) { + throw new ServiceException("运单里程维护数据不能为空"); + } + Waybill waybill = loadEditable(request.getId(), true); + if (!"completed".equals(waybill.getBusinessStatus())) { + throw new ServiceException("仅已完成运单允许维护里程"); + } + if (receivablePayableDetailService.settlementLinkedWaybillIds(List.of(waybill.getId())) + .contains(waybill.getId())) { + throw new ServiceException("该运单已生成结算单,无法维护里程"); + } + BigDecimal mileage = request.getMileage(); + if (mileage == null || mileage.compareTo(BigDecimal.ZERO) <= 0 + || mileage.stripTrailingZeros().scale() > 0 || mileage.stripTrailingZeros().precision() > 10) { + throw new ServiceException("里程必须为不超过10位的正整数"); + } + String mileageRemark = TransportBusinessSupport.trimToNull(request.getMileageRemark()); + TransportBusinessSupport.validateLength(mileageRemark, 200, "里程维护备注不能超过200字"); + waybill.setMileage(mileage); + waybill.setMileageRemark(mileageRemark); + return updateById(waybill); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean cancel(Long id) { @@ -504,6 +538,22 @@ public class WaybillServiceImpl extends BaseServiceImpl return queryWrapper; } + private void fillMileageMaintainable(List waybills) { + waybills.forEach(item -> item.setMileageMaintainable(false)); + List completedIds = waybills.stream() + .filter(item -> "completed".equals(item.getBusinessStatus())) + .map(Waybill::getId) + .filter(Objects::nonNull) + .toList(); + if (completedIds.isEmpty()) { + return; + } + Set settlementLinkedIds = receivablePayableDetailService.settlementLinkedWaybillIds(completedIds); + waybills.stream() + .filter(item -> completedIds.contains(item.getId())) + .forEach(item -> item.setMileageMaintainable(!settlementLinkedIds.contains(item.getId()))); + } + private void prepare(Waybill waybill) { if (isSentinelMinusOne(waybill.getQuantity())) waybill.setQuantity(null); if (isSentinelMinusOne(waybill.getMileage())) waybill.setMileage(null); diff --git a/doc/sql/transport/blade_receivable_payable_cargo_fee_billing_rules_20260830.sql b/doc/sql/transport/blade_receivable_payable_cargo_fee_billing_rules_20260830.sql new file mode 100644 index 0000000..5bcfde3 --- /dev/null +++ b/doc/sql/transport/blade_receivable_payable_cargo_fee_billing_rules_20260830.sql @@ -0,0 +1,28 @@ +-- MySQL 5.7+ 兼容:保存应收应付费用生成时实际命中的计费规则。 +-- 请在 transport 数据库执行。本脚本使用 information_schema 判断,可重复执行。 + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS `upgrade_receivable_payable_cargo_fee_billing_rules_20260830`$$ +CREATE PROCEDURE `upgrade_receivable_payable_cargo_fee_billing_rules_20260830`() +BEGIN + DECLARE db_name varchar(128); + SET db_name = DATABASE(); + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = db_name + AND table_name = 'blade_receivable_payable_cargo_fee' + AND column_name = 'billing_rules_json' + ) THEN + ALTER TABLE `blade_receivable_payable_cargo_fee` + ADD COLUMN `billing_rules_json` text COLLATE utf8mb4_general_ci DEFAULT NULL + COMMENT '命中计费规则JSON' AFTER `billing_type`; + END IF; +END$$ + +CALL `upgrade_receivable_payable_cargo_fee_billing_rules_20260830`()$$ +DROP PROCEDURE `upgrade_receivable_payable_cargo_fee_billing_rules_20260830`$$ + +DELIMITER ; diff --git a/doc/sql/transport/blade_receivable_payable_detail_20260812.sql b/doc/sql/transport/blade_receivable_payable_detail_20260812.sql index 5e77871..bb7ecab 100644 --- a/doc/sql/transport/blade_receivable_payable_detail_20260812.sql +++ b/doc/sql/transport/blade_receivable_payable_detail_20260812.sql @@ -64,13 +64,14 @@ CREATE TABLE IF NOT EXISTS `blade_receivable_payable_cargo_fee` ( `detail_id` bigint(20) NOT NULL COMMENT '应收应付明细ID', `waybill_id` bigint(20) DEFAULT NULL COMMENT '运单ID', `line_no` varchar(30) DEFAULT NULL COMMENT '行号', - `data_source` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '自动生成' COMMENT '来源:自动生成/手工录入', + `data_source` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '自动生成' COMMENT '来源:自动生成/手动录入', `cargo_name` varchar(100) DEFAULT NULL COMMENT '货物名称', `cargo_type` varchar(100) DEFAULT NULL COMMENT '货物类型', `specification` varchar(255) DEFAULT NULL COMMENT '规格', `model` varchar(255) DEFAULT NULL COMMENT '型号', `billing_factor` varchar(100) DEFAULT NULL COMMENT '计费要素', `billing_type` varchar(100) DEFAULT NULL COMMENT '计费类型', + `billing_rules_json` text COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '命中计费规则JSON', `transport_quantity` decimal(18,6) DEFAULT NULL COMMENT '运输量', `quantity_unit` varchar(50) DEFAULT NULL COMMENT '数量单位', `price_unit` varchar(50) DEFAULT NULL COMMENT '运费计算单位', diff --git a/doc/sql/transport/blade_settlement_adjustment_20260818.sql b/doc/sql/transport/blade_settlement_adjustment_20260818.sql index 71c66db..3576194 100644 --- a/doc/sql/transport/blade_settlement_adjustment_20260818.sql +++ b/doc/sql/transport/blade_settlement_adjustment_20260818.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS `blade_settlement_adjustment` ( `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', - `adjustment_no` varchar(100) NOT NULL COMMENT '结算调整单号', `formal_settlement_id` bigint(20) NOT NULL COMMENT '关联正式结算单ID', - `formal_settlement_no` varchar(100) NOT NULL COMMENT '关联正式结算单号', `settlement_type` varchar(30) NOT NULL, + `adjustment_no` varchar(100) NOT NULL COMMENT '结算调整单号', `formal_settlement_id` bigint(20) DEFAULT NULL COMMENT '关联正式结算单ID', + `formal_settlement_no` varchar(100) DEFAULT NULL COMMENT '关联正式结算单号', `settlement_type` varchar(30) DEFAULT NULL, `project_name` varchar(100) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `customer_name` varchar(200) DEFAULT NULL, `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(100) DEFAULT NULL, `adjustment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', @@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `blade_settlement_adjustment_detail` ( `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', - `adjustment_id` bigint(20) NOT NULL, `formal_settlement_detail_id` bigint(20) NOT NULL, `formal_settlement_detail_fee_id` bigint(20) NOT NULL, + `adjustment_id` bigint(20) NOT NULL, `formal_settlement_detail_id` bigint(20) DEFAULT NULL, `formal_settlement_detail_fee_id` bigint(20) DEFAULT NULL, `fee_type` varchar(100) DEFAULT NULL, `fee_item` varchar(200) DEFAULT NULL, `original_amount_tax` decimal(18,2) DEFAULT NULL, `adjustment_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00', `adjustment_amount_no_tax` decimal(18,2) DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_adjustment_detail_bill` (`adjustment_id`), diff --git a/doc/sql/transport/blade_settlement_adjustment_draft_nullable_20260831.sql b/doc/sql/transport/blade_settlement_adjustment_draft_nullable_20260831.sql new file mode 100644 index 0000000..4b561a3 --- /dev/null +++ b/doc/sql/transport/blade_settlement_adjustment_draft_nullable_20260831.sql @@ -0,0 +1,5 @@ +-- 结算调整单草稿允许暂不完善关联正式结算信息 +ALTER TABLE `blade_settlement_adjustment` + MODIFY COLUMN `formal_settlement_id` bigint(20) DEFAULT NULL COMMENT '关联正式结算单ID', + MODIFY COLUMN `formal_settlement_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '关联正式结算单号', + MODIFY COLUMN `settlement_type` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL; diff --git a/doc/sql/transport/blade_settlement_adjustment_manual_fee_20260830.sql b/doc/sql/transport/blade_settlement_adjustment_manual_fee_20260830.sql new file mode 100644 index 0000000..34e69a1 --- /dev/null +++ b/doc/sql/transport/blade_settlement_adjustment_manual_fee_20260830.sql @@ -0,0 +1,5 @@ +-- 结算调整单支持手工新增费用 + +ALTER TABLE `blade_settlement_adjustment_detail` + MODIFY COLUMN `formal_settlement_detail_id` bigint(20) DEFAULT NULL COMMENT '正式结算明细ID,手工费用为空', + MODIFY COLUMN `formal_settlement_detail_fee_id` bigint(20) DEFAULT NULL COMMENT '正式结算明细费用ID,手工费用为空'; diff --git a/doc/sql/transport/blade_tms_business.sql b/doc/sql/transport/blade_tms_business.sql index a892e3c..92632b8 100644 --- a/doc/sql/transport/blade_tms_business.sql +++ b/doc/sql/transport/blade_tms_business.sql @@ -262,6 +262,7 @@ CREATE TABLE `blade_waybill` ( `escort_name` varchar(100) DEFAULT NULL COMMENT '押运人', `escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号', `mileage` decimal(18,2) DEFAULT NULL COMMENT '里程', + `mileage_remark` varchar(200) DEFAULT NULL COMMENT '里程维护备注', `estimated_start_time` date DEFAULT NULL COMMENT '预计发货日期', `estimated_end_time` date DEFAULT NULL COMMENT '预计完成日期', `unit_price` decimal(18,2) DEFAULT NULL COMMENT '单价', @@ -415,6 +416,7 @@ INSERT IGNORE INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `pa (2090000000000000611, 2090000000000000600, 'waybill_manage_import', '导入运单', 'waybill_manage_import', '', '', 11, 2, 0, 1, NULL, '', 0), (2090000000000000612, 2090000000000000600, 'waybill_manage_template', '下载模板', 'waybill_manage_template', '', '', 12, 2, 0, 1, NULL, '', 0), (2090000000000000613, 2090000000000000600, 'waybill_manage_road_loading', '公路配载', 'waybill_manage_road_loading', '', '', 13, 2, 0, 1, NULL, '', 0), +(2090000000000000614, 2090000000000000600, 'waybill_manage_mileage', '维护里程', 'waybill_manage_mileage', '', '', 14, 2, 0, 1, NULL, '', 0), (2090000000000000700, 2090000000000000000, 'loading_manage', '配载管理', 'loading_manage', '/business/loading-manage', 'iconfont icon-caidanguanli', 70, 1, 0, 1, NULL, '', 0), (2090000000000000701, 2090000000000000700, 'loading_manage_view', '查看', 'loading_manage_view', '', '', 1, 2, 0, 1, NULL, '', 0), (2090000000000000702, 2090000000000000700, 'loading_manage_add', '新增', 'loading_manage_add', '', '', 2, 2, 0, 1, NULL, '', 0), diff --git a/doc/sql/transport/blade_waybill_mileage_remark_20260831.sql b/doc/sql/transport/blade_waybill_mileage_remark_20260831.sql new file mode 100644 index 0000000..0b55e24 --- /dev/null +++ b/doc/sql/transport/blade_waybill_mileage_remark_20260831.sql @@ -0,0 +1,8 @@ +ALTER TABLE `blade_waybill` + ADD COLUMN `mileage_remark` varchar(200) DEFAULT NULL COMMENT '里程维护备注' AFTER `mileage`; + +INSERT INTO `blade_menu` +(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) +VALUES +(2090000000000000614, 2090000000000000600, 'waybill_manage_mileage', '维护里程', 'waybill_manage_mileage', '', '', 14, 2, 0, 1, NULL, '', 0) +ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `alias` = VALUES(`alias`), `sort` = VALUES(`sort`), `is_deleted` = 0; From 27ed35f9ddf55ed19aecf4dc4de3387a7cc8aef8 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 31 Aug 2026 12:40:27 +0800 Subject: [PATCH 059/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=94=B6=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/vo/ReceiptFlowVO.java | 5 +++ .../impl/PaymentApplicationServiceImpl.java | 33 ++++-------------- .../impl/PreSettlementServiceImpl.java | 34 ++++++++++++++++++- .../impl/ReceiptClaimRecordServiceImpl.java | 27 +++++++++++++-- .../service/impl/ReceiptFlowServiceImpl.java | 5 +++ 5 files changed, 75 insertions(+), 29 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java index caa11ce..b282c74 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java @@ -30,12 +30,15 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springblade.transport.pojo.entity.ReceiptFlowRecord; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serial; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; /** * 收款流水视图实体类 @@ -70,4 +73,6 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow { @TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime transactionEndTime; + @TableField(exist = false) + private List claimRecords = new ArrayList<>(); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index f8195ac..d67defb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -73,7 +73,6 @@ import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Stream; /** 付款申请服务实现。 @author Chill */ @Service @@ -114,6 +113,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl { PaymentApplicationVO vo = PaymentApplicationWrapper.build().entityVO(item); + if (!APPROVED.equals(item.getApprovalStatus())) vo.setPaidAmount(BigDecimal.ZERO); return vo; }); } @@ -471,15 +471,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery().eq(ProjectApply::getId, entity.getProjectId()).last("FOR UPDATE")); if (project != null && project.getFundLimit() != null) { BigDecimal projectLimit = projectFundLimit(project); - BigDecimal projectOccupiedAmount = quotaOccupiedAmount(Wrappers.lambdaQuery() - .eq(PaymentApplication::getProjectId, entity.getProjectId()) - .ne(entity.getId() != null, PaymentApplication::getId, entity.getId()), entity); - if (projectOccupiedAmount.compareTo(projectLimit) > 0) { - throw new ServiceException("申请付款金额超过项目剩余资金使用额度(已扣减收票金额并包含在途申请)"); + if (appliedAmount.compareTo(projectLimit) > 0) { + throw new ServiceException("申请付款金额不能超过项目额度"); } } if (Func.isNotEmpty(entity.getPayeeName())) { @@ -488,13 +487,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl customerNames = Stream.of(customer.getFullName(), customer.getShortName()) - .filter(Func::isNotEmpty).map(String::trim).distinct().toList(); - BigDecimal customerOccupiedAmount = quotaOccupiedAmount(Wrappers.lambdaQuery() - .in(PaymentApplication::getPayeeName, customerNames) - .ne(entity.getId() != null, PaymentApplication::getId, entity.getId()), entity); - if (customerOccupiedAmount.compareTo(customerLimit) > 0) { - throw new ServiceException("申请付款金额超过客户剩余资金使用额度(已扣减收票金额并包含在途申请)"); + if (appliedAmount.compareTo(customerLimit) > 0) { + throw new ServiceException("申请付款金额不能超过客户额度"); } } } @@ -512,19 +506,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl wrapper, - PaymentApplication currentApplication) { - List applications = list(wrapper.ne(PaymentApplication::getApprovalStatus, VOIDED) - .last("FOR UPDATE")); - BigDecimal appliedAmount = applications.stream().map(PaymentApplication::getAppliedAmount) - .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add) - .add(money(currentApplication.getAppliedAmount())); - BigDecimal receivedInvoiceAmount = applications.stream().map(PaymentApplication::getMatchedInvoiceAmount) - .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add) - .add(money(currentApplication.getMatchedInvoiceAmount())); - return appliedAmount.subtract(receivedInvoiceAmount).max(BigDecimal.ZERO); - } - private BigDecimal sumApplied(LambdaQueryWrapper wrapper) { return list(wrapper.ne(PaymentApplication::getApprovalStatus, VOIDED)).stream() .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 06fafe7..360ad1f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -42,6 +42,7 @@ import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementDetailMapper; import org.springblade.transport.mapper.PreSettlementMapper; import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper; +import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest; @@ -56,6 +57,7 @@ import org.springblade.transport.pojo.entity.PreSettlementChangeRecord; import org.springblade.transport.pojo.entity.PreSettlementDetail; import org.springblade.transport.pojo.entity.PreSettlementDetailFee; import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; +import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; @@ -113,6 +115,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl selectPage(IPage page, PreSettlementVO query) { - return PreSettlementWrapper.build().pageVO(page(page, buildQuery(query))); + IPage result = PreSettlementWrapper.build().pageVO(page(page, buildQuery(query))); + fillPaymentApplicationAmounts(result.getRecords()); + return result; + } + + private void fillPaymentApplicationAmounts(List records) { + if (Func.isEmpty(records)) return; + Set preSettlementIds = records.stream().map(PreSettlementVO::getId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + if (preSettlementIds.isEmpty()) return; + List applications = paymentApplicationMapper.selectList( + Wrappers.lambdaQuery() + .select(PaymentApplication::getPreSettlementId, PaymentApplication::getAppliedAmount, + PaymentApplication::getPaidAmount, PaymentApplication::getApprovalStatus) + .eq(PaymentApplication::getPaymentType, "progress_advance") + .in(PaymentApplication::getPreSettlementId, preSettlementIds) + .in(PaymentApplication::getApprovalStatus, STATUS_REVIEWING, STATUS_APPROVED) + .eq(PaymentApplication::getIsDeleted, 0)); + Map reviewingAmounts = new HashMap<>(); + Map approvedAmounts = new HashMap<>(); + applications.forEach(application -> { + boolean reviewing = STATUS_REVIEWING.equals(application.getApprovalStatus()); + Map amounts = reviewing ? reviewingAmounts : approvedAmounts; + BigDecimal amount = reviewing ? application.getAppliedAmount() : application.getPaidAmount(); + amounts.merge(application.getPreSettlementId(), money(amount), BigDecimal::add); + }); + records.forEach(record -> { + record.setAdvanceAppliedAmount(money(reviewingAmounts.get(record.getId()))); + record.setAdvancePaidAmount(money(approvedAmounts.get(record.getId()))); + }); } @Override diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java index bcf456a..3affe44 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java @@ -54,7 +54,9 @@ import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -94,14 +96,35 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl settlements = claimSettlementMapper.selectList( Wrappers.lambdaQuery() .eq(ReceiptClaimSettlement::getReceiptClaimId, id) - .orderByAsc(ReceiptClaimSettlement::getCreateTime))); + .orderByAsc(ReceiptClaimSettlement::getCreateTime)); + fillSettlementClaimedAmounts(settlements); + record.setSettlements(settlements); fillStatusNames(record); return record; } + private void fillSettlementClaimedAmounts(List settlements) { + List settlementIds = settlements.stream() + .map(ReceiptClaimSettlement::getFormalSettlementId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (settlementIds.isEmpty()) { + return; + } + Map claimedAmountMap = new HashMap<>(); + claimSettlementMapper.selectList(Wrappers.lambdaQuery() + .in(ReceiptClaimSettlement::getFormalSettlementId, settlementIds) + .eq(ReceiptClaimSettlement::getStatus, 1)) + .forEach(relation -> claimedAmountMap.merge(relation.getFormalSettlementId(), + money(relation.getAllocatedReceiptAmount()), BigDecimal::add)); + settlements.forEach(settlement -> settlement.setClaimedReceiptAmount( + money(claimedAmountMap.get(settlement.getFormalSettlementId())))); + } + @Override @Transactional(rollbackFor = Exception.class) public void updateAttachments(ReceiptClaimAttachmentsRequest request) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java index e36bc06..8834724 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java @@ -111,6 +111,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpllambdaQuery() + .eq(ReceiptFlowRecord::getReceiptFlowId, id) + .eq(ReceiptFlowRecord::getActionType, "claim") + .orderByDesc(ReceiptFlowRecord::getCreateTime))); return vo; } From 35309b32aee459304f24d7aac3bacd6c8012803c Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 31 Aug 2026 14:55:50 +0800 Subject: [PATCH 060/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=94=B6=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InvoiceApplicationController.java | 7 +- .../service/IInvoiceApplicationService.java | 2 +- .../impl/InvoiceApplicationServiceImpl.java | 98 +++++++++++++++---- 3 files changed, 84 insertions(+), 23 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java index 35ddac0..4a817b4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java @@ -80,8 +80,11 @@ public class InvoiceApplicationController extends BladeController { @GetMapping("/settlement-candidates") @ApiOperationSupport(order = 3) @Operation(summary = "可开票正式结算单") - public R>> settlementCandidates(@RequestParam(required = false) String keyword) { - return R.data(invoiceApplicationService.settlementCandidates(keyword)); + public R>> settlementCandidates(Query query, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String contractCategory) { + return R.data(invoiceApplicationService.settlementCandidates( + Condition.getPage(query), keyword, contractCategory)); } @GetMapping("/settlement-details") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java index fe65e70..9434bfc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java @@ -44,7 +44,7 @@ import java.util.Map; public interface IInvoiceApplicationService extends BaseService { IPage selectPage(IPage page, InvoiceApplicationVO query); InvoiceApplicationVO detail(Long id); - List> settlementCandidates(String keyword); + IPage> settlementCandidates(IPage page, String keyword, String contractCategory); List settlementDetails(String settlementIds); Map receiverInformation(String settlementIds); Long saveDraft(InvoiceApplicationSaveRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index eff9419..431a5e1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -28,6 +28,7 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; @@ -37,6 +38,7 @@ import org.springblade.core.tool.utils.Func; import org.springblade.transport.mapper.CustomerArchiveMapper; import org.springblade.transport.mapper.CustomerContactMapper; import org.springblade.transport.mapper.CustomerInvoiceInfoMapper; +import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.InvoiceApplicationDetailMapper; @@ -50,6 +52,7 @@ import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest; import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.entity.CustomerContact; import org.springblade.transport.pojo.entity.CustomerInvoiceInfo; +import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.InvoiceApplication; @@ -102,6 +105,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl> settlementCandidates(String keyword) { + public IPage> settlementCandidates(IPage page, String keyword, + String contractCategory) { + List contractIds = Func.isEmpty(contractCategory) ? List.of() + : contractManageMapper.selectList(Wrappers.lambdaQuery() + .select(ContractManage::getId) + .eq(ContractManage::getContractCategory, contractCategory) + .eq(ContractManage::getIsDeleted, 0)) + .stream().map(ContractManage::getId).toList(); + if (Func.isNotEmpty(contractCategory) && contractIds.isEmpty()) { + return new Page<>(page.getCurrent(), page.getSize(), 0); + } List settlements = formalSettlementMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlement::getSettlementType, "payable") .eq(FormalSettlement::getApprovalStatus, APPROVED) .eq(FormalSettlement::getStatus, 1) + .in(Func.isNotEmpty(contractCategory), FormalSettlement::getContractId, contractIds) .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(FormalSettlement::getFormalSettlementNo, keyword) .or().like(FormalSettlement::getProjectName, keyword) .or().like(FormalSettlement::getContractName, keyword)) - .orderByDesc(FormalSettlement::getCreateTime).last("limit 200")); - return settlements.stream().map(settlement -> { - BigDecimal available = availableAmount(settlement, null); - Map row = new LinkedHashMap<>(); - row.put("id", settlement.getId()); - row.put("formalSettlementNo", settlement.getFormalSettlementNo()); - row.put("projectId", settlement.getProjectId()); - row.put("projectName", settlement.getProjectName()); - row.put("deptId", settlement.getDeptId()); - row.put("deptName", settlement.getDeptName()); - row.put("contractId", settlement.getContractId()); - row.put("contractNo", settlement.getContractNo()); - row.put("contractName", settlement.getContractName()); - row.put("issuerName", settlement.getPayeeName()); - row.put("receiverName", settlement.getPayerName()); - row.put("settlementAmount", money(settlement.getSettlementAmount())); - row.put("availableInvoiceAmount", available); - return row; - }).filter(row -> ((BigDecimal) row.get("availableInvoiceAmount")).compareTo(BigDecimal.ZERO) > 0).toList(); + .orderByDesc(FormalSettlement::getCreateTime)); + Map availableAmounts = candidateAvailableAmounts(settlements); + List> candidates = settlements.stream() + .map(settlement -> settlementCandidateMap(settlement, + availableAmounts.getOrDefault(settlement.getId(), BigDecimal.ZERO))) + .filter(row -> ((BigDecimal) row.get("availableInvoiceAmount")).compareTo(BigDecimal.ZERO) > 0) + .toList(); + long current = Math.max(page.getCurrent(), 1); + long size = Math.max(page.getSize(), 1); + long offset = Math.min((current - 1) * size, candidates.size()); + long end = Math.min(offset + size, candidates.size()); + Page> result = new Page<>(current, size, candidates.size()); + result.setRecords(candidates.subList((int) offset, (int) end)); + return result; + } + + private Map candidateAvailableAmounts(List settlements) { + if (settlements.isEmpty()) return Map.of(); + List settlementIds = settlements.stream().map(FormalSettlement::getId).toList(); + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .in(InvoiceApplicationSettlement::getFormalSettlementId, settlementIds)); + if (relations.isEmpty()) { + return settlements.stream().collect(Collectors.toMap(FormalSettlement::getId, + settlement -> money(settlement.getSettlementAmount()), (first, duplicate) -> first, + LinkedHashMap::new)); + } + Map applications = listByIds(relations.stream() + .map(InvoiceApplicationSettlement::getInvoiceApplicationId).distinct().toList()).stream() + .collect(Collectors.toMap(InvoiceApplication::getId, Function.identity())); + Map allocatedAmounts = relations.stream() + .filter(relation -> { + InvoiceApplication application = applications.get(relation.getInvoiceApplicationId()); + return application != null && !VOIDED.equals(application.getApprovalStatus()) + && !Objects.equals(application.getIsDeleted(), 1); + }) + .collect(Collectors.groupingBy(InvoiceApplicationSettlement::getFormalSettlementId, + Collectors.reducing(BigDecimal.ZERO, + relation -> money(relation.getAllocatedInvoiceAmount()), BigDecimal::add))); + return settlements.stream().collect(Collectors.toMap(FormalSettlement::getId, + settlement -> money(settlement.getSettlementAmount()) + .subtract(allocatedAmounts.getOrDefault(settlement.getId(), BigDecimal.ZERO)) + .max(BigDecimal.ZERO), + (first, duplicate) -> first, LinkedHashMap::new)); + } + + private Map settlementCandidateMap(FormalSettlement settlement, BigDecimal available) { + Map row = new LinkedHashMap<>(); + row.put("id", settlement.getId()); + row.put("formalSettlementNo", settlement.getFormalSettlementNo()); + row.put("projectId", settlement.getProjectId()); + row.put("projectName", settlement.getProjectName()); + row.put("deptId", settlement.getDeptId()); + row.put("deptName", settlement.getDeptName()); + row.put("contractId", settlement.getContractId()); + row.put("contractNo", settlement.getContractNo()); + row.put("contractName", settlement.getContractName()); + row.put("issuerName", settlement.getPayeeName()); + row.put("receiverName", settlement.getPayerName()); + row.put("settlementAmount", money(settlement.getSettlementAmount())); + row.put("availableInvoiceAmount", available); + return row; } @Override From d1ef6eb97bceed6a6a42adc0b198ae2ebbcf5c0a Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 31 Aug 2026 16:11:34 +0800 Subject: [PATCH 061/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=94=B6=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InvoiceApplicationController.java | 6 +++-- .../service/IInvoiceApplicationService.java | 3 ++- .../impl/InvoiceApplicationServiceImpl.java | 26 ++++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java index 4a817b4..c3ac9cc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java @@ -82,9 +82,11 @@ public class InvoiceApplicationController extends BladeController { @Operation(summary = "可开票正式结算单") public R>> settlementCandidates(Query query, @RequestParam(required = false) String keyword, - @RequestParam(required = false) String contractCategory) { + @RequestParam(required = false) String contractCategory, + @RequestParam(defaultValue = "receivable") String settlementType, + @RequestParam(defaultValue = "unreceived") String invoiceStatus) { return R.data(invoiceApplicationService.settlementCandidates( - Condition.getPage(query), keyword, contractCategory)); + Condition.getPage(query), keyword, contractCategory, settlementType, invoiceStatus)); } @GetMapping("/settlement-details") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java index 9434bfc..93fe9fa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java @@ -44,7 +44,8 @@ import java.util.Map; public interface IInvoiceApplicationService extends BaseService { IPage selectPage(IPage page, InvoiceApplicationVO query); InvoiceApplicationVO detail(Long id); - IPage> settlementCandidates(IPage page, String keyword, String contractCategory); + IPage> settlementCandidates(IPage page, String keyword, String contractCategory, + String settlementType, String invoiceStatus); List settlementDetails(String settlementIds); Map receiverInformation(String settlementIds); Long saveDraft(InvoiceApplicationSaveRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index 431a5e1..b3edd8c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -147,7 +147,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl> settlementCandidates(IPage page, String keyword, - String contractCategory) { + String contractCategory, String settlementType, String invoiceStatus) { + String candidateSettlementType = Func.isEmpty(settlementType) ? "receivable" : settlementType; + String candidateInvoiceStatus = Func.isEmpty(invoiceStatus) ? "unreceived" : invoiceStatus; List contractIds = Func.isEmpty(contractCategory) ? List.of() : contractManageMapper.selectList(Wrappers.lambdaQuery() .select(ContractManage::getId) @@ -158,7 +160,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl(page.getCurrent(), page.getSize(), 0); } List settlements = formalSettlementMapper.selectList(Wrappers.lambdaQuery() - .eq(FormalSettlement::getSettlementType, "payable") + .eq(FormalSettlement::getSettlementType, candidateSettlementType) + .eq(FormalSettlement::getInvoiceStatus, candidateInvoiceStatus) .eq(FormalSettlement::getApprovalStatus, APPROVED) .eq(FormalSettlement::getStatus, 1) .in(Func.isNotEmpty(contractCategory), FormalSettlement::getContractId, contractIds) @@ -243,13 +246,15 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl settlements = distinctIds(settlementIds).stream().map(this::availableSettlement).toList(); assertCompatible(settlements); FormalSettlement first = settlements.get(0); - CustomerArchive customer = findCustomer(first.getPayeeName()); + String customerName = "receivable".equals(first.getSettlementType()) + ? first.getPayerName() : first.getPayeeName(); + CustomerArchive customer = findCustomer(customerName); List invoiceInfos = customer == null ? List.of() : activeInvoiceInfos(customer.getId()); Map result = new LinkedHashMap<>(); result.put("issuerName", first.getPayeeName()); result.put("receiverName", first.getPayerName()); result.put("customer", customer); - // 应付结算单的开票方(payeeName)是客商,受票方信息和部门邮箱均来源于该客商发票信息。 + // 应收结算单的受票方(payerName)是客商;历史应付申请仍按开票方(payeeName)读取客商资料。 result.put("invoices", invoiceInfos); result.put("invoiceInfos", invoiceInfos); result.put("departmentEmails", invoiceInfoEmails(invoiceInfos)); @@ -274,6 +279,10 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl first, LinkedHashMap::new)).values().stream().toList(); List settlements = requestedRows.stream().map(row -> availableSettlement(row.getSettlementId())).toList(); + if (settlements.stream().anyMatch(settlement -> !"receivable".equals(settlement.getSettlementType()) + || (creating && !"unreceived".equals(settlement.getInvoiceStatus())))) { + throw new ServiceException("只能选择审批通过、未作废、未收票的应收正式结算单"); + } assertCompatible(settlements); FormalSettlement first = settlements.get(0); Map settlementMap = settlements.stream() @@ -292,8 +301,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl invoiceInfos = activeInvoiceInfos(customer.getId()); CustomerInvoiceInfo invoiceInfo = invoiceInfos.stream() .filter(item -> Objects.equals(item.getId(), request.getReceiverInvoiceInfoId())) @@ -475,9 +484,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl Date: Mon, 31 Aug 2026 17:04:01 +0800 Subject: [PATCH 062/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=94=B6=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FormalSettlementController.java | 9 ++++ .../service/IReceiptFlowService.java | 2 + .../impl/PreSettlementServiceImpl.java | 1 + .../service/impl/ReceiptFlowServiceImpl.java | 50 +++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index b8d2420..ef083be 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -28,6 +28,7 @@ import org.springblade.transport.pojo.vo.FormalSettlementVO; import org.springblade.transport.pojo.vo.PreSettlementVO; import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IPreSettlementService; +import org.springblade.transport.service.IReceiptFlowService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -51,6 +52,7 @@ import java.util.Map; public class FormalSettlementController extends BladeController { private final IFormalSettlementService formalSettlementService; private final IPreSettlementService preSettlementService; + private final IReceiptFlowService receiptFlowService; @GetMapping("/list") @ApiOperationSupport(order = 1) @@ -166,4 +168,11 @@ public class FormalSettlementController extends BladeController { public R> applyPayments(@RequestBody FormalSettlementBatchPaymentRequest request) { return R.data(formalSettlementService.applyPayments(request)); } + + @GetMapping("/receipt-claims") + @ApiOperationSupport(order = 19) + @Operation(summary = "应收正式结算单收款认领信息") + public R>> receiptClaims(@RequestParam Long formalSettlementId) { + return R.data(receiptFlowService.settlementClaims(formalSettlementId)); + } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java index 816990d..4fe2905 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java @@ -48,6 +48,8 @@ public interface IReceiptFlowService extends BaseService { List> settlementCandidates(String keyword, Long flowId); + List> settlementClaims(Long formalSettlementId); + Long claim(ReceiptClaimRequest request); int sync(ReceiptFlowSyncRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 360ad1f..537c720 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -183,6 +183,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl details = listDetails(id); vo.setDetails(details); List detailIds = details.stream().map(PreSettlementDetail::getId).toList(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java index 8834724..c78f80f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java @@ -151,6 +151,56 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl> settlementClaims(Long formalSettlementId) { + if (formalSettlementId == null) { + throw new ServiceException("正式结算单ID不能为空"); + } + FormalSettlement settlement = formalSettlementMapper.selectById(formalSettlementId); + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) { + throw new ServiceException("正式结算单不存在"); + } + List relations = claimSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getFormalSettlementId, formalSettlementId) + .eq(ReceiptClaimSettlement::getStatus, 1) + .orderByDesc(ReceiptClaimSettlement::getCreateTime)); + if (relations.isEmpty()) { + return List.of(); + } + List claimIds = relations.stream().map(ReceiptClaimSettlement::getReceiptClaimId) + .filter(Objects::nonNull).distinct().toList(); + List flowIds = relations.stream().map(ReceiptClaimSettlement::getReceiptFlowId) + .filter(Objects::nonNull).distinct().toList(); + Map claims = claimIds.isEmpty() ? Map.of() : claimMapper.selectBatchIds(claimIds) + .stream().collect(Collectors.toMap(ReceiptClaim::getId, Function.identity())); + Map flows = flowIds.isEmpty() ? Map.of() : listByIds(flowIds) + .stream().collect(Collectors.toMap(KingdeeReceiptFlow::getId, Function.identity())); + return relations.stream().map(relation -> { + ReceiptClaim claim = claims.get(relation.getReceiptClaimId()); + KingdeeReceiptFlow flow = flows.get(relation.getReceiptFlowId()); + if (claim == null || flow == null) { + return null; + } + Map row = new LinkedHashMap<>(); + row.put("receiptClaimId", claim.getId()); + row.put("receiptFlowId", flow.getId()); + row.put("receiptNoticeNo", flow.getReceiptNoticeNo()); + row.put("payerName", flow.getPayerName()); + row.put("receiptAmount", money(flow.getReceiptAmount())); + row.put("allocatedReceiptAmount", money(relation.getAllocatedReceiptAmount())); + row.put("transactionTime", flow.getTransactionTime()); + row.put("counterpartyName", flow.getCounterpartyName()); + row.put("detailSerialNo", flow.getDetailSerialNo()); + row.put("claimerName", claim.getClaimerName()); + row.put("claimerDeptName", claim.getClaimerDeptName()); + row.put("claimDate", claim.getClaimDate()); + row.put("claimStatus", claim.getClaimStatus()); + row.put("claimStatusName", CLAIMED.equals(claim.getClaimStatus()) ? "已认领" : "已作废"); + return row; + }).filter(Objects::nonNull).toList(); + } + @Override @Transactional(rollbackFor = Exception.class) public Long claim(ReceiptClaimRequest request) { From 1363a526ffb9b8628a159bbfc028b793f22a307b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 1 Sep 2026 11:52:15 +0800 Subject: [PATCH 063/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=94=B6=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/mapper/AirportMasterMapper.java | 10 +++ .../system/mapper/AirportMasterMapper.xml | 14 ++++ .../impl/AirportMasterServiceImpl.java | 17 +++++ .../impl/FormalSettlementServiceImpl.java | 64 ++++++++----------- .../impl/PreSettlementServiceImpl.java | 19 +++--- 5 files changed, 75 insertions(+), 49 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.java index cf65428..3a0e6a3 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.java @@ -40,6 +40,16 @@ import java.util.List; */ public interface AirportMasterMapper extends BaseMapper { + /** + * 按编码查询空港机场(包含逻辑删除记录,用于唯一性校验)。 + */ + AirportMaster selectByCodeIncludingDeleted(@Param("code") String code); + + /** + * 恢复逻辑删除空港机场。 + */ + int restoreById(@Param("id") Long id); + /** * 自定义分页 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.xml index f5e14e9..79a2fdc 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AirportMasterMapper.xml @@ -32,6 +32,20 @@ + + + + UPDATE blade_airport_master + SET is_deleted = 0 + WHERE id = #{id} + AND is_deleted = 1 + + - + SELECT + region.code, + region.name, + region.parent_code, + CASE WHEN region.parent_code = '0' THEN '根节点' + ELSE (SELECT parent.name FROM blade_region parent WHERE parent.code = region.parent_code) + END AS parent_name, + region.region_level, + CASE region.status WHEN 1 THEN '启用' WHEN 2 THEN '停用' ELSE '' END AS status_name, + region.data_source, + COALESCE(updater.real_name, creator.real_name) AS update_user_name, + region.update_time, + region.create_time + FROM blade_region region + LEFT JOIN blade_user updater ON updater.id = region.update_user + LEFT JOIN blade_user creator ON creator.id = region.create_user + ${ew.customSqlSegment} + ORDER BY region.create_time DESC, region.code ASC diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IFeeItemService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IFeeItemService.java index a0f018f..65b02d6 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IFeeItemService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IFeeItemService.java @@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.system.excel.FeeItemExcel; +import org.springblade.system.excel.FeeItemExportExcel; import org.springblade.system.excel.FeeItemImportFailureExcel; import org.springblade.system.pojo.entity.FeeItem; import org.springblade.system.pojo.vo.FeeItemVO; @@ -82,6 +83,6 @@ public interface IFeeItemService extends BaseService { * @param queryWrapper 查询条件 * @return 导出数据 */ - List exportFeeItem(Wrapper queryWrapper); + List exportFeeItem(Wrapper queryWrapper); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IRegionService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IRegionService.java index 339bc16..653f78c 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IRegionService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IRegionService.java @@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.extension.service.IService; import org.springblade.system.pojo.entity.Region; import org.springblade.system.excel.RegionExcel; +import org.springblade.system.excel.RegionExportExcel; import org.springblade.system.pojo.vo.RegionVO; import java.util.List; @@ -90,6 +91,6 @@ public interface IRegionService extends IService { * @param queryWrapper * @return */ - List exportRegion(Wrapper queryWrapper); + List exportRegion(Wrapper queryWrapper); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java index 656b2f0..c077730 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java @@ -34,6 +34,7 @@ import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.UserCache; import org.springblade.system.excel.AirportMasterExcel; import org.springblade.system.excel.AirportMasterExportExcel; import org.springblade.system.mapper.AirportMasterMapper; @@ -319,6 +320,7 @@ public class AirportMasterServiceImpl extends BaseServiceImpl } @Override - public List exportFeeItem(Wrapper queryWrapper) { + public List exportFeeItem(Wrapper queryWrapper) { return list(queryWrapper).stream().map(this::toExcel).toList(); } @@ -193,9 +196,14 @@ public class FeeItemServiceImpl extends BaseServiceImpl return feeItem; } - private FeeItemExcel toExcel(FeeItem feeItem) { - FeeItemExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(feeItem, FeeItemExcel.class)); + private FeeItemExportExcel toExcel(FeeItem feeItem) { + FeeItemExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(feeItem, FeeItemExportExcel.class)); excel.setFeeCategory(formatFeeCategory(feeItem.getFeeCategory())); + excel.setCreateDeptName( + Func.isEmpty(feeItem.getCreateDept()) ? "" : SysCache.getDeptName(feeItem.getCreateDept()) + ); + excel.setUpdateUserName(UserCache.getUserRealName(feeItem.getUpdateUser())); + excel.setStatusName(Objects.equals(feeItem.getStatus(), STATUS_ENABLED) ? "启用" : "停用"); return excel; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java index 4057b9e..937973a 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java @@ -34,6 +34,7 @@ import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.UserCache; import org.springblade.system.excel.PortTerminalExcel; import org.springblade.system.excel.PortTerminalExportExcel; import org.springblade.system.mapper.PortTerminalMapper; @@ -392,6 +393,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl 1) { - addValidationError(validationErrors, "电报码在本次导入中重复"); + addValidationError(validationErrors, "电报略码在本次导入中重复"); } - validateImportUnique(RailwayStation::getTelegraphCode, railwayStation.getTelegraphCode(), "电报码已存在", validationErrors); + validateImportUnique(RailwayStation::getTelegraphCode, railwayStation.getTelegraphCode(), "电报略码已存在", validationErrors); } if (Func.isEmpty(railwayStation.getName())) { addValidationError(validationErrors, "车站名称不能为空"); @@ -392,10 +392,10 @@ public class RailwayStationServiceImpl extends BaseServiceImpl impleme @Override public boolean submit(Region region) { + Date now = new Date(); + Long currentUserId = AuthUtil.getUserId(); + boolean isNew = StringUtil.isBlank(region.getOriginalCode()); + if (region.getStatus() == null) { + region.setStatus(1); + } + if (StringUtil.isBlank(region.getDataSource())) { + region.setDataSource("手动录入"); + } + if (isNew) { + region.setCreateUser(currentUserId); + region.setCreateTime(now); + } + region.setUpdateUser(currentUserId); + region.setUpdateTime(now); String regionCode = region.getCode(); String regionParentCode = region.getParentCode(); Integer level = region.getRegionLevel(); + validateRegionLevel(level); if (level != null && level == COUNTRY_LEVEL) { region.setParentCode(ROOT_PARENT_CODE); region.setAncestors(ROOT_PARENT_CODE); @@ -86,17 +105,29 @@ public class RegionServiceImpl extends ServiceImpl impleme region.setAncestors(ancestors); } else if (MAIN_CODE.equals(region.getParentCode())) { region.setAncestors(MAIN_CODE); + } else if (ROOT_PARENT_CODE.equals(region.getParentCode())) { + region.setAncestors(ROOT_PARENT_CODE); } - // 设置省、市、区、镇、村 + // 设置省、市、区、镇、村,并继承上级区划信息 String code = region.getCode(); String name = region.getName(); if (level == PROVINCE_LEVEL) { region.setProvinceCode(code); region.setProvinceName(name); } else if (level == CITY_LEVEL) { + if (Func.isNotEmpty(parent)) { + region.setProvinceCode(parent.getProvinceCode()); + region.setProvinceName(parent.getProvinceName()); + } region.setCityCode(code); region.setCityName(name); } else if (level == DISTRICT_LEVEL) { + if (Func.isNotEmpty(parent)) { + region.setProvinceCode(parent.getProvinceCode()); + region.setProvinceName(parent.getProvinceName()); + region.setCityCode(parent.getCityCode()); + region.setCityName(parent.getCityName()); + } region.setDistrictCode(code); region.setDistrictName(name); } else if (level == TOWN_LEVEL) { @@ -128,6 +159,12 @@ public class RegionServiceImpl extends ServiceImpl impleme } } + private void validateRegionLevel(Integer level) { + if (level != null && level > DISTRICT_LEVEL) { + throw new ServiceException("区划等级仅支持国家、省份/直辖市、地市、区县"); + } + } + @Override public boolean removeRegion(String id) { Long cnt = baseMapper.selectCount(Wrappers.query().lambda().eq(Region::getParentCode, id)); @@ -162,11 +199,18 @@ public class RegionServiceImpl extends ServiceImpl impleme RegionExcel excel = data.get(index); try { Region region = BeanUtil.copyProperties(excel, Region.class); - if (Boolean.TRUE.equals(isCovered)) { - cacheChanged = this.saveOrUpdate(region) || cacheChanged; - } else { - cacheChanged = this.save(region) || cacheChanged; + validateRegionLevel(region.getRegionLevel()); + region.setSort(index + 1); + if (region.getStatus() == null) { + region.setStatus(1); } + if (StringUtil.isBlank(region.getDataSource())) { + region.setDataSource("初始化导入"); + } + if (Boolean.TRUE.equals(isCovered) && this.getById(region.getCode()) != null) { + region.setOriginalCode(region.getCode()); + } + cacheChanged = this.submit(region) || cacheChanged; } catch (Exception exception) { excel.setErrorMessage(exception.getMessage()); errorList.add(excel); @@ -179,7 +223,11 @@ public class RegionServiceImpl extends ServiceImpl impleme } @Override - public List exportRegion(Wrapper queryWrapper) { - return baseMapper.exportRegion(queryWrapper); + public List exportRegion(Wrapper queryWrapper) { + List list = baseMapper.exportRegion(queryWrapper); + for (int index = 0; index < list.size(); index++) { + list.get(index).setSerialNumber(index + 1); + } + return list; } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java index 853fa58..2802c14 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java @@ -77,6 +77,16 @@ public class ContractManageExcel implements Serializable { private String contractStage; @ExcelProperty("审核状态") private String approvalStatus; + @ExcelProperty("结算币种") + private String settlementCurrency; + @ExcelProperty("结算方式") + private String settlementMode; + @ExcelProperty("开票周期(天)") + private Integer invoiceCycle; + @ExcelProperty("一式份数") + private Integer copyCount; + @ExcelProperty("回款账期(天)") + private Integer paymentDays; @ExcelProperty("当前节点") private String currentNode; @ExcelProperty("当前处理人") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml index b11fd95..da48c1d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml @@ -137,6 +137,10 @@ AND driver_name LIKE #{driverNameLike} + + + AND posts LIKE #{postsLike} + AND mobile LIKE #{mobileLike} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index d6d1479..f572c84 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -222,6 +222,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl implements IMasterOrderService { + private static final String CARRIER_SELF = "自运"; + private final IWaybillService waybillService; private final ITransportPlanService transportPlanService; private final IProjectApplyService projectApplyService; @@ -418,7 +420,9 @@ public class MasterOrderServiceImpl extends BaseServiceImpl quantityUnits = goods.stream() + .map(item -> item instanceof JSONObject ? ((JSONObject) item).getString("quantityUnit") : null) + .map(TransportBusinessSupport::trimToNull) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (freightItems == null || (freightItems.size() != goods.size() && freightItems.size() != quantityUnits.size())) { + throw new ServiceException("公路运输的运费明细必须覆盖货物信息"); } + boolean groupedByQuantityUnit = freightItems.size() == quantityUnits.size() && freightItems.size() != goods.size(); BigDecimal totalFreightAmount = BigDecimal.ZERO; for (int index = 0; index < freightItems.size(); index++) { JSONObject freightItem = freightItems.getJSONObject(index); if (freightItem == null) { throw new ServiceException("第" + (index + 1) + "条运费明细格式不正确"); } + String quantityUnit = TransportBusinessSupport.trimToNull(freightItem.getString("quantityUnit")); + if (groupedByQuantityUnit) { + quantityUnit = quantityUnit == null && index < quantityUnits.size() ? quantityUnits.get(index) : quantityUnit; + } + JSONObject goodsItem = groupedByQuantityUnit ? null : goods.getJSONObject(index); + if (quantityUnit == null && goodsItem != null) { + quantityUnit = TransportBusinessSupport.trimToNull(goodsItem.getString("quantityUnit")); + } + if (quantityUnit == null) { + throw new ServiceException("第" + (index + 1) + "条运费明细数量单位不能为空"); + } + final String itemQuantityUnit = quantityUnit; + BigDecimal quantity = groupedByQuantityUnit + ? goods.stream() + .map(item -> item instanceof JSONObject ? (JSONObject) item : null) + .filter(item -> itemQuantityUnit.equals(TransportBusinessSupport.trimToNull(item.getString("quantityUnit")))) + .map(item -> amountValue(item.get("quantity"), "货物数量")) + .reduce(BigDecimal.ZERO, BigDecimal::add) + : amountValue(goodsItem == null ? null : goodsItem.get("quantity"), "第" + (index + 1) + "条货物数量"); BigDecimal unitPrice = amountValue(freightItem.get("unitPrice"), "第" + (index + 1) + "条货物单价"); String priceUnit = TransportBusinessSupport.trimToNull(freightItem.getString("priceUnit")); if (StringUtil.isBlank(priceUnit)) { throw new ServiceException("第" + (index + 1) + "条货物计价单位不能为空"); } - JSONObject goodsItem = goods.getJSONObject(index); - BigDecimal quantity = amountValue(goodsItem == null ? null : goodsItem.get("quantity"), "第" + (index + 1) + "条货物数量"); - BigDecimal freightAmount = unitPrice.multiply(quantity); + BigDecimal calculatedAmount = unitPrice.multiply(quantity); + Object inputAmount = freightItem.get("freightAmount"); + boolean autoCalculable = quantityUnits.size() == 1 && priceUnitMatchesQuantity(priceUnit, itemQuantityUnit) + && freightItem.get("unitPrice") != null && StringUtil.isNotBlank(String.valueOf(freightItem.get("unitPrice"))); + if (!autoCalculable && (inputAmount == null || StringUtil.isBlank(String.valueOf(inputAmount)))) { + throw new ServiceException("第" + (index + 1) + "条货物运费不能为空"); + } + boolean manualAmount = Boolean.TRUE.equals(freightItem.getBoolean("manualFreightAmount")) + || Boolean.TRUE.equals(freightItem.getBoolean("freightAmountManual")); + if (!manualAmount && inputAmount != null && StringUtil.isNotBlank(String.valueOf(inputAmount))) { + BigDecimal enteredAmount = amountValue(inputAmount, "第" + (index + 1) + "条货物运费"); + manualAmount = enteredAmount.compareTo(calculatedAmount) != 0; + } + BigDecimal freightAmount = manualAmount + ? amountValue(inputAmount, "第" + (index + 1) + "条货物运费") + : calculatedAmount; freightItem.put("cargoIndex", index); + freightItem.put("quantityUnit", quantityUnit); freightItem.put("unitPrice", decimalText(unitPrice)); freightItem.put("priceUnit", priceUnit); freightItem.put("quantity", decimalText(quantity)); freightItem.put("freightAmount", decimalText(freightAmount)); + freightItem.put("manualFreightAmount", manualAmount); totalFreightAmount = totalFreightAmount.add(freightAmount); } - freight.put("totalFreightAmount", decimalText(totalFreightAmount)); + BigDecimal otherFreightAmount = amountValue(freight.get("otherFreightAmount"), "其他运费合计"); + freight.put("totalFreightAmount", decimalText(totalFreightAmount.add(otherFreightAmount))); freight.remove("freightAmount"); freight.remove("quantity"); } else { @@ -371,9 +413,11 @@ public class ShippingTemplateServiceImpl extends BaseServiceImpl amountValue(item == null ? null : item.get("quantity"), "货物数量")) .reduce(BigDecimal.ZERO, BigDecimal::add); freight.put("quantity", decimalText(quantity)); - freight.put("freightAmount", amountText(freight.get("freightAmount"), "运费")); + String freightAmount = amountText(freight.get("freightAmount"), "运费"); + freight.put("freightAmount", freightAmount); + BigDecimal otherFreightAmount = amountValue(freight.get("otherFreightAmount"), "其他运费合计"); + freight.put("totalFreightAmount", decimalText(new BigDecimal(freightAmount).add(otherFreightAmount))); freight.remove("freightItems"); - freight.remove("totalFreightAmount"); } shippingTemplate.setFreightJson(freight.toJSONString()); } @@ -383,6 +427,26 @@ public class ShippingTemplateServiceImpl extends BaseServiceImpl implements ITransportPlanService { private final IWaybillService waybillService; + private final IContractManageService contractManageService; @Override public IPage selectTransportPlanPage(IPage page, TransportPlanVO transportPlan) { @@ -85,10 +88,30 @@ public class TransportPlanServiceImpl extends BaseServiceImpl query = new LambdaQueryWrapper() + .eq(ContractManage::getContractName, transportPlan.getContractName()); + if (Func.isNotEmpty(transportPlan.getProjectId())) { + query.eq(ContractManage::getProjectId, transportPlan.getProjectId()); + } + return contractManageService.getOne(query, false); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean submit(TransportPlan transportPlan) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 0c98c6a..5497b7e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -39,6 +39,7 @@ import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ProcessConfig; +import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.dto.WaybillMileageRequest; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; @@ -46,6 +47,7 @@ import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IProcessConfigService; +import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IWaybillService; @@ -81,6 +83,9 @@ public class WaybillServiceImpl extends BaseServiceImpl @jakarta.annotation.Resource private IProcessConfigService processConfigService; + @jakarta.annotation.Resource + private IProjectApplyService projectApplyService; + @jakarta.annotation.Resource private IContractManageService contractManageService; @@ -135,11 +140,22 @@ public class WaybillServiceImpl extends BaseServiceImpl fillProjectProcessConfig(waybill); } prepare(waybill); + fillCustomerName(waybill); if (created && Func.isEmpty(waybill.getWaybillNo())) { waybill.setWaybillNo(nextCode()); } } + private void fillCustomerName(Waybill waybill) { + if (Func.isNotEmpty(waybill.getCustomerName()) || Func.isEmpty(waybill.getProjectId())) { + return; + } + ProjectApply project = projectApplyService.getById(waybill.getProjectId()); + if (project != null) { + waybill.setCustomerName(TransportBusinessSupport.trimToNull(project.getCustomerNames())); + } + } + private void fillProjectProcessConfig(Waybill waybill) { if (Func.isNotEmpty(waybill.getProcessJson()) || Func.isEmpty(waybill.getProjectId())) { return; @@ -605,7 +621,7 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setTaskRemark(TransportBusinessSupport.trimToNull(waybill.getTaskRemark())); waybill.setOriginalNo(TransportBusinessSupport.trimToNull(waybill.getOriginalNo())); waybill.setBusinessStatus(TransportBusinessSupport.trimToNull(waybill.getBusinessStatus())); - waybill.setDataSource(TransportBusinessSupport.trimToNull(waybill.getDataSource())); + waybill.setDataSource(TransportBusinessSupport.normalizeWaybillDataSource(waybill.getDataSource())); waybill.setPlanName(TransportBusinessSupport.trimToNull(waybill.getPlanName())); waybill.setMasterNo(TransportBusinessSupport.trimToNull(waybill.getMasterNo())); waybill.setLoadingNo(TransportBusinessSupport.trimToNull(waybill.getLoadingNo())); @@ -652,12 +668,6 @@ public class WaybillServiceImpl extends BaseServiceImpl } else { TransportBusinessSupport.validateRequired(waybill.getDriverName(), "司机不能为空"); TransportBusinessSupport.validateRequired(waybill.getDriverPhone(), "司机手机号不能为空"); - TransportBusinessSupport.validateRequired(waybill.getTrailerVehicleNo(), "挂车车牌号不能为空"); - TransportBusinessSupport.validateRequired(waybill.getEscortName(), "押运人不能为空"); - TransportBusinessSupport.validateRequired(waybill.getEscortPhone(), "押运人手机号不能为空"); - if (Func.isEmpty(waybill.getMileage())) { - throw new ServiceException("里程不能为空"); - } } } TransportBusinessSupport.validateRequired(waybill.getCarrierJson(), "承运信息不能为空"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java index 54bf8eb..1a6082e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java @@ -43,6 +43,7 @@ public final class TransportBusinessSupport { public static final String DATA_SOURCE_BATCH_IMPORT = "批量导入"; public static final String DATA_SOURCE_MANUAL = "手工创建"; + public static final String DATA_SOURCE_PLAN_DISPATCH = "计划调度"; public static final String DATA_SOURCE_EXTERNAL = "外部系统"; private static final Pattern PHONE_PATTERN = Pattern.compile("^(1\\d{10}|0\\d{2,3}-?\\d{7,8})$"); @@ -110,6 +111,23 @@ public final class TransportBusinessSupport { }; } + /** + * 归一化运单数据来源,保证列表只出现约定的三种来源。 + * + * @param value 原始数据来源 + * @return 批量导入、手工创建或计划调度 + */ + public static String normalizeWaybillDataSource(String value) { + String source = trimToNull(value); + if (DATA_SOURCE_BATCH_IMPORT.equals(source)) { + return DATA_SOURCE_BATCH_IMPORT; + } + if (DATA_SOURCE_PLAN_DISPATCH.equals(source) || "多联总单调度".equals(source)) { + return DATA_SOURCE_PLAN_DISPATCH; + } + return DATA_SOURCE_MANUAL; + } + public static void validateRequired(String value, String message) { if (Func.isEmpty(trimToNull(value))) { throw new ServiceException(message); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java index ce89125..ed06b57 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java @@ -29,6 +29,7 @@ import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserCache; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.WaybillVO; +import org.springblade.transport.support.TransportBusinessSupport; import java.util.Objects; @@ -48,6 +49,7 @@ public class WaybillWrapper extends BaseEntityWrapper { WaybillVO waybillVO = Objects.requireNonNull(BeanUtil.copyProperties(waybill, WaybillVO.class)); waybillVO.setCreateUserName(UserCache.getUserRealName(waybill.getCreateUser())); waybillVO.setUpdateUserName(UserCache.getUserRealName(waybill.getUpdateUser())); + waybillVO.setDataSource(TransportBusinessSupport.normalizeWaybillDataSource(waybill.getDataSource())); Long currentDeptId = Func.firstLong(AuthUtil.getDeptId()); waybillVO.setReadonly(currentDeptId != null && !Objects.equals(waybill.getDeptId(), currentDeptId)); waybillVO.setBusinessStatusName(businessStatusName(waybill.getBusinessStatus())); diff --git a/doc/sql/transport/blade_contract_manage_settlement_fields_20260902.sql b/doc/sql/transport/blade_contract_manage_settlement_fields_20260902.sql new file mode 100644 index 0000000..71f60c7 --- /dev/null +++ b/doc/sql/transport/blade_contract_manage_settlement_fields_20260902.sql @@ -0,0 +1,4 @@ +-- 合同管理补充结算币种、开票周期字段 +ALTER TABLE `blade_contract_manage` + ADD COLUMN `settlement_currency` varchar(50) DEFAULT NULL COMMENT '结算币种' AFTER `copy_count`, + ADD COLUMN `invoice_cycle` int(11) DEFAULT NULL COMMENT '开票周期(天)' AFTER `settlement_currency`; diff --git a/doc/sql/transport/blade_project_contract_management.sql b/doc/sql/transport/blade_project_contract_management.sql index b8502c8..2b9b7b1 100644 --- a/doc/sql/transport/blade_project_contract_management.sql +++ b/doc/sql/transport/blade_project_contract_management.sql @@ -133,6 +133,8 @@ CREATE TABLE `blade_contract_manage` ( `contract_format` varchar(50) DEFAULT NULL COMMENT '合同格式', `legal_seal_flag` int(11) DEFAULT '0' COMMENT '是否需要加盖法人章', `copy_count` int(11) DEFAULT NULL COMMENT '一式份数', + `settlement_currency` varchar(50) DEFAULT NULL COMMENT '结算币种', + `invoice_cycle` int(11) DEFAULT NULL COMMENT '开票周期(天)', `payment_days` int(11) DEFAULT NULL COMMENT '回款账期(天)', `contract_stage` varchar(50) DEFAULT NULL COMMENT '合同阶段', `approval_status` varchar(50) DEFAULT NULL COMMENT '审核状态', diff --git a/doc/sql/transport/blade_region_export_fields_20260903.sql b/doc/sql/transport/blade_region_export_fields_20260903.sql new file mode 100644 index 0000000..eb52a1c --- /dev/null +++ b/doc/sql/transport/blade_region_export_fields_20260903.sql @@ -0,0 +1,79 @@ +-- 行政区划导出字段补充:支持状态、数据来源及审计信息。 +-- 可重复执行,字段已存在时不会重复添加。 + +SET @db_name = DATABASE(); + +SET @sql = IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @db_name AND table_name = 'blade_region' AND column_name = 'data_source' + ), + 'SELECT 1', + 'ALTER TABLE `blade_region` ADD COLUMN `data_source` varchar(20) DEFAULT ''初始化导入'' COMMENT ''数据来源'' AFTER `remark`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @db_name AND table_name = 'blade_region' AND column_name = 'create_user' + ), + 'SELECT 1', + 'ALTER TABLE `blade_region` ADD COLUMN `create_user` bigint(20) DEFAULT NULL COMMENT ''创建人'' AFTER `data_source`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @db_name AND table_name = 'blade_region' AND column_name = 'create_time' + ), + 'SELECT 1', + 'ALTER TABLE `blade_region` ADD COLUMN `create_time` datetime DEFAULT NULL COMMENT ''创建时间'' AFTER `create_user`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @db_name AND table_name = 'blade_region' AND column_name = 'update_user' + ), + 'SELECT 1', + 'ALTER TABLE `blade_region` ADD COLUMN `update_user` bigint(20) DEFAULT NULL COMMENT ''更新人'' AFTER `create_time`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @db_name AND table_name = 'blade_region' AND column_name = 'update_time' + ), + 'SELECT 1', + 'ALTER TABLE `blade_region` ADD COLUMN `update_time` datetime DEFAULT NULL COMMENT ''更新时间'' AFTER `update_user`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @db_name AND table_name = 'blade_region' AND column_name = 'status' + ), + 'SELECT 1', + 'ALTER TABLE `blade_region` ADD COLUMN `status` int(11) DEFAULT 1 COMMENT ''状态'' AFTER `update_time`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +UPDATE `blade_region` SET `data_source` = '初始化导入' WHERE `data_source` IS NULL OR TRIM(`data_source`) = ''; +UPDATE `blade_region` SET `status` = 1 WHERE `status` IS NULL; diff --git a/doc/sql/transport/blade_region_level_patch_20260903.sql b/doc/sql/transport/blade_region_level_patch_20260903.sql new file mode 100644 index 0000000..7f569f3 --- /dev/null +++ b/doc/sql/transport/blade_region_level_patch_20260903.sql @@ -0,0 +1,6 @@ +-- 行政区划等级仅保留:国家、省份/直辖市、地市、区县。 +-- 删除乡镇(4)和村委(5)字典项;已有历史区域数据不做删除。 + +DELETE FROM `blade_dict` +WHERE `code` = 'region' + AND (`dict_key` IN ('4', '5') OR `dict_value` IN ('乡镇', '村委')); diff --git a/doc/sql/transport/transport.sql b/doc/sql/transport/transport.sql index 2218256..7209c9d 100644 --- a/doc/sql/transport/transport.sql +++ b/doc/sql/transport/transport.sql @@ -565,8 +565,6 @@ INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, ` INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738777232, 1123598814738777230, 'region', '1', '省份/直辖市', 1, NULL, 0, 1, 0); INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738777233, 1123598814738777230, 'region', '2', '地市', 2, NULL, 0, 1, 0); INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738777234, 1123598814738777230, 'region', '3', '区县', 3, NULL, 0, 1, 0); -INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738777235, 1123598814738777230, 'region', '4', '乡镇', 4, NULL, 0, 1, 0); -INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738777236, 1123598814738777230, 'region', '5', '村委', 5, NULL, 0, 1, 0); INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738778200, 0, 'user_type', '-1', '用户平台', 14, NULL, 0, 1, 0); INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738778201, 1123598814738778200, 'user_type', '1', 'WEB', 1, NULL, 0, 1, 0); INSERT INTO `blade_dict` (`id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`) VALUES (1123598814738778202, 1123598814738778200, 'user_type', '2', 'APP', 2, NULL, 0, 1, 0); @@ -1581,6 +1579,12 @@ CREATE TABLE `blade_region` ( `region_level` int(11) DEFAULT NULL COMMENT '层级', `sort` int(11) DEFAULT NULL COMMENT '排序', `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `data_source` varchar(20) DEFAULT '初始化导入' COMMENT '数据来源', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `status` int(11) DEFAULT '1' COMMENT '状态', PRIMARY KEY (`code`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='行政区划表'; From a8d866231b34b7607a1d3e14272b1bfa3fe86836 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 3 Sep 2026 15:59:20 +0800 Subject: [PATCH 068/114] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E9=85=8D=E7=BD=AE=202=E3=80=81=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/pojo/entity/PortTerminal.java | 10 +++ .../system/excel/PortTerminalExcel.java | 3 + .../system/excel/PortTerminalExportExcel.java | 3 + .../system/mapper/PortTerminalMapper.java | 11 ++++ .../system/mapper/PortTerminalMapper.xml | 23 +++++++ .../system/mapper/RailwayStationMapper.java | 10 +++ .../system/mapper/RailwayStationMapper.xml | 13 ++++ .../impl/AirportMasterServiceImpl.java | 17 +++-- .../service/impl/PortTerminalServiceImpl.java | 64 ++++++++++++++++++- .../impl/RailwayStationServiceImpl.java | 32 ++++++++-- doc/sql/bladex/bladex.mysql.all.create.sql | 4 +- doc/sql/transport/blade_port_terminal.sql | 4 +- .../blade_port_terminal_province_20260903.sql | 16 +++++ doc/sql/transport/transport.sql | 4 +- 14 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 doc/sql/transport/blade_port_terminal_province_20260903.sql diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/PortTerminal.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/PortTerminal.java index d68c174..c768e26 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/PortTerminal.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/PortTerminal.java @@ -84,6 +84,16 @@ public class PortTerminal extends BaseEntity { */ @Schema(description = "国家") private String country; + /** + * 省份编码 + */ + @Schema(description = "省份编码") + private String provinceCode; + /** + * 省份 + */ + @Schema(description = "省份") + private String provinceName; /** * 城市 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java index 5044906..1786f85 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExcel.java @@ -74,6 +74,9 @@ public class PortTerminalExcel implements Serializable { @ExcelProperty("国家*") private String country; + @ExcelProperty("所属省份*") + private String provinceName; + @ExcelProperty("城市*") private String city; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java index 44761ac..b1c6471 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java @@ -48,6 +48,9 @@ public class PortTerminalExportExcel implements Serializable { @ExcelProperty("国家") private String country; + @ExcelProperty("所属省份") + private String provinceName; + @ExcelProperty("城市") private String city; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.java index 1b4031d..ff68f78 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.java @@ -27,6 +27,7 @@ package org.springblade.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Param; import org.springblade.system.pojo.entity.PortTerminal; import org.springblade.system.pojo.vo.PortTerminalVO; @@ -39,6 +40,16 @@ import java.util.List; */ public interface PortTerminalMapper extends BaseMapper { + /** + * 按编码查询港口码头(包含逻辑删除记录,用于导入恢复)。 + */ + PortTerminal selectByCodeIncludingDeleted(@Param("code") String code); + + /** + * 恢复逻辑删除港口码头。 + */ + int restoreById(@Param("id") Long id); + /** * 自定义分页 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.xml index d881bee..d86b165 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/PortTerminalMapper.xml @@ -19,6 +19,8 @@ + + @@ -30,6 +32,19 @@ + + + + UPDATE blade_port_terminal + SET is_deleted = 0 + WHERE id = #{id} + AND is_deleted = 1 + + + SELECT * + FROM blade_railway_station + WHERE code = #{code} + + + + UPDATE blade_railway_station + SET is_deleted = 0 + WHERE id = #{id} + AND is_deleted = 1 + + diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java index 9529647..92c253a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java @@ -2,6 +2,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.VoucherManageChangeBatchRequest; import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest; import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest; import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest; @@ -22,6 +23,7 @@ public interface IVoucherManageService extends BaseService { void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType); void removeFolder(Long voucherId, String plateNo); void submit(VoucherManageSubmitRequest request); + void changeWaybillBatch(VoucherManageChangeBatchRequest request); VoucherManage createUploadDraft(VoucherUploadDraftRequest request); void completeUploadFile(VoucherFileCompleteRequest request); void processUploadedVoucher(Long voucherId); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java index ef88a08..3b4d519 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java @@ -14,6 +14,7 @@ import org.springblade.transport.mapper.VoucherImageMapper; import org.springblade.transport.mapper.VoucherFileMapper; import org.springblade.transport.mapper.VoucherWaybillBatchMapper; import org.springblade.transport.event.VoucherUploadCompletedEvent; +import org.springblade.transport.pojo.dto.VoucherManageChangeBatchRequest; import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest; import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest; import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest; @@ -101,7 +102,17 @@ public class VoucherManageServiceImpl extends BaseServiceImpl selectPage(IPage page, VoucherManageVO query) { - return VoucherManageWrapper.build().pageVO(page(page, buildQuery(query))); + IPage result = VoucherManageWrapper.build().pageVO(page(page, buildQuery(query))); + // 凭证数量按文件夹(车牌目录)统计,兼容历史记录中仍保存图片数量的旧数据。 + if (result.getRecords() != null) { + result.getRecords().forEach(item -> { + VoucherFolderCounts folderCounts = countVoucherFolderRelations(item.getTenantId(), item.getId()); + item.setVoucherCount(folderCounts.total()); + item.setRelatedWaybillCount(folderCounts.related()); + item.setUnRelatedWaybillCount(folderCounts.unrelated()); + }); + } + return result; } @Override @@ -154,9 +165,13 @@ public class VoucherManageServiceImpl extends BaseServiceImpl 50L * 1024 * 1024) throw new ServiceException("凭证文件大小不能超过50M"); + if (enforceSizeLimit && uploadFile.getSize() > 50L * 1024 * 1024) throw new ServiceException("凭证文件大小不能超过50M"); String normalizedPlateNo = normalizePlateNo(plateNo); if (Func.isEmpty(normalizedPlateNo)) throw new ServiceException("车牌号不能为空"); validateMinioConfig(); @@ -200,26 +215,135 @@ public class VoucherManageServiceImpl extends BaseServiceImpl candidateObjectKeys = resolvedObjectKey.equals(objectKey) + ? List.of(objectKey) : List.of(resolvedObjectKey, objectKey); + for (String candidateObjectKey : candidateObjectKeys) { + try (InputStream source = minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build()); + OutputStream target = Files.newOutputStream(archivePath)) { + source.transferTo(target); + return archivePath; + } catch (Exception exception) { + lastException = exception; + log.warn("读取系统上传对象失败,尝试下一个对象路径 bucket={}, objectKey={}", minioBucketName, candidateObjectKey, exception); + } + } + deleteTempArchive(archivePath); + log.error("读取系统上传文件失败 bucket={}, objectKey={}, candidateObjectKeys={}", + minioBucketName, objectKey, candidateObjectKeys, lastException); + throw new ServiceException("读取系统上传文件失败"); + } + + private String resolveMinioObjectKey(String objectKey) { + if (Func.isEmpty(minioRootDirectory) || Func.isEmpty(objectKey)) { + return objectKey; + } + String normalizedRoot = minioRootDirectory.endsWith("/") + ? minioRootDirectory.substring(0, minioRootDirectory.length() - 1) : minioRootDirectory; + return objectKey.equals(normalizedRoot) || objectKey.startsWith(normalizedRoot + "/") + ? objectKey : normalizedRoot + "/" + objectKey; + } + + private void deleteSourceObjectQuietly(String objectKey) { + deleteObjectQuietly(resolveMinioObjectKey(objectKey)); + } + + private MultipartFile localArchiveMultipartFile(Path archivePath, String fileName, String contentType) throws java.io.IOException { + long archiveSize = Files.size(archivePath); + return new MultipartFile() { @Override public String getName() { return "file"; } @Override public String getOriginalFilename() { return fileName; } - @Override public String getContentType() { return contentType; } - @Override public boolean isEmpty() { return size != null && size == 0; } - @Override public long getSize() { return size == null ? -1L : size; } - @Override public byte[] getBytes() throws java.io.IOException { try (InputStream input = getInputStream()) { return input.readAllBytes(); } } - @Override public InputStream getInputStream() throws java.io.IOException { - try { return minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } - catch (Exception exception) { throw new java.io.IOException("读取系统上传文件失败", exception); } + @Override public String getContentType() { return Func.isEmpty(contentType) ? "application/zip" : contentType; } + @Override public boolean isEmpty() { return archiveSize == 0; } + @Override public long getSize() { return archiveSize; } + @Override public byte[] getBytes() throws java.io.IOException { return Files.readAllBytes(archivePath); } + @Override public InputStream getInputStream() throws java.io.IOException { return Files.newInputStream(archivePath); } + @Override public void transferTo(java.io.File destination) throws java.io.IOException { + try (InputStream input = getInputStream(); OutputStream output = Files.newOutputStream(destination.toPath())) { + input.transferTo(output); + } } - @Override public void transferTo(java.io.File dest) throws java.io.IOException { try (InputStream input = getInputStream(); OutputStream output = Files.newOutputStream(dest.toPath())) { input.transferTo(output); } } }; - try { - replaceFolder(voucherId, plateNo, uploadedFile); - } finally { - // 系统上传接口产生的临时附件仅用于本次替换,复制到凭证目录后清理源对象。 - deleteObjectQuietly(objectKey); + } + + private String resolveReplacementPlateNo(VoucherManage voucher, String requestedPlateNo, String archiveFileName, Path archivePath) throws Exception { + String normalizedRequested = normalizePlateNo(requestedPlateNo); + Set existingPlates = new HashSet<>(); + voucherFileMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucher.getId()) + .eq(VoucherFile::getIsDeleted, 0)).forEach(file -> addPlateNumber(existingPlates, file.getPlateNo())); + voucherImageMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId()) + .eq(VoucherImage::getIsDeleted, 0)).forEach(image -> addPlateNumber(existingPlates, image.getPlateNo())); + Map waybillByPlate = new HashMap<>(); + for (Waybill waybill : listRelatedWaybills(voucher)) { + for (String waybillPlateNo : waybillPlateNumbers(waybill)) { + waybillByPlate.putIfAbsent(waybillPlateNo, waybill); + } } + if (Func.isNotEmpty(normalizedRequested) && waybillByPlate.containsKey(normalizedRequested)) { + return normalizedRequested; + } + String archiveNamePlate = normalizePlateNo(archiveFileName.replaceFirst("(?i)\\.zip$", "")); + if (Func.isNotEmpty(archiveNamePlate) && (waybillByPlate.containsKey(archiveNamePlate) || existingPlates.contains(archiveNamePlate))) { + return archiveNamePlate; + } + Charset archiveCharset = detectArchiveCharset(archivePath); + Set archivePlates = new HashSet<>(); + try (InputStream source = Files.newInputStream(archivePath); + ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), archiveCharset)) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + List pathParts = Arrays.stream(entry.getName().replace('\\', '/').split("/")) + .filter(Func::isNotEmpty).toList(); + if (pathParts.isEmpty()) continue; + String folderName = resolvePlateFolderName(pathParts, waybillByPlate); + String archivePlate = normalizePlateNo(folderName); + if (Func.isNotEmpty(archivePlate)) archivePlates.add(archivePlate); + } + } + Set matchedArchivePlates = archivePlates.stream().filter(waybillByPlate::containsKey).collect(Collectors.toSet()); + if (matchedArchivePlates.size() == 1) return matchedArchivePlates.iterator().next(); + Set existingArchivePlates = archivePlates.stream().filter(existingPlates::contains).collect(Collectors.toSet()); + if (existingArchivePlates.size() == 1) return existingArchivePlates.iterator().next(); + if (archivePlates.size() == 1 && !"凭证导入".equals(archivePlates.iterator().next())) { + return archivePlates.iterator().next(); + } + if (Func.isNotEmpty(normalizedRequested)) return normalizedRequested; + throw new ServiceException("无法从压缩包目录识别车牌号"); } @Override @@ -448,16 +572,40 @@ public class VoucherManageServiceImpl extends BaseServiceImpl images = voucherImageMapper.selectList(Wrappers.lambdaQuery() - .eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId()).eq(VoucherImage::getIsDeleted, 0)); - long relatedWaybillCount = images.stream().filter(item -> Objects.equals(item.getMatched(), 1)) - .map(VoucherImage::getWaybillId).filter(Objects::nonNull).distinct().count(); - int waybillCount = listRelatedWaybills(voucher).size(); - VoucherManage update = new VoucherManage(); update.setId(voucher.getId()); update.setVoucherCount(images.size()); - update.setRelatedWaybillCount((int) relatedWaybillCount); update.setUnRelatedWaybillCount(Math.max(waybillCount - (int) relatedWaybillCount, 0)); + VoucherFolderCounts folderCounts = countVoucherFolderRelations(voucher.getTenantId(), voucher.getId()); + VoucherManage update = new VoucherManage(); update.setId(voucher.getId()); update.setVoucherCount(folderCounts.total()); + update.setRelatedWaybillCount(folderCounts.related()); update.setUnRelatedWaybillCount(folderCounts.unrelated()); update.setProcessStatus("处理完成"); updateById(update); } + private int countVoucherFolders(VoucherManage voucher) { + return countVoucherFolderRelations(voucher.getTenantId(), voucher.getId()).total(); + } + + private VoucherFolderCounts countVoucherFolderRelations(String tenantId, Long voucherId) { + List files = voucherFileMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherFile::getTenantId, tenantId).eq(VoucherFile::getVoucherId, voucherId).eq(VoucherFile::getIsDeleted, 0)); + if (!files.isEmpty()) { + Set folders = files.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + Set relatedFolders = files.stream().filter(item -> Objects.equals(item.getMatched(), 1)) + .map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + return new VoucherFolderCounts(folders.size(), relatedFolders.size(), folders.size() - relatedFolders.size()); + } + List images = voucherImageMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherImage::getTenantId, tenantId).eq(VoucherImage::getVoucherId, voucherId).eq(VoucherImage::getIsDeleted, 0)); + Set folders = images.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + Set relatedFolders = images.stream().filter(item -> Objects.equals(item.getMatched(), 1)) + .map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + return new VoucherFolderCounts(folders.size(), relatedFolders.size(), folders.size() - relatedFolders.size()); + } + + private record VoucherFolderCounts(int total, int related, int unrelated) {} + + private String folderKey(String plateNo) { + String normalizedPlateNo = normalizePlateNo(plateNo); + return Func.isEmpty(normalizedPlateNo) ? "未识别车牌" : normalizedPlateNo; + } + private void deleteObjectQuietly(String objectKey) { try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } } @Override @@ -478,6 +626,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl> batches = selectableWaybillBatchesByIds(request.getWaybillImportBatchIds()); + if (batches.size() != request.getWaybillImportBatchIds().size()) throw new ServiceException("存在无效的运输批次"); + + // 更换批次必须同步重建关联关系,否则后续匹配仍会读取旧批次。 + voucherWaybillBatchMapper.deletePhysicalByVoucherId(voucher.getId()); + List relations = new ArrayList<>(); + for (Map batch : batches) { + VoucherWaybillBatch relation = new VoucherWaybillBatch(); + relation.setVoucherId(voucher.getId()); + relation.setWaybillImportBatchId(((Number) batch.get("id")).longValue()); + relation.setWaybillBatchNo((String) batch.get("batchNo")); + relation.setWaybillCount(((Number) batch.get("waybillCount")).intValue()); + relations.add(relation); + } + relations.forEach(voucherWaybillBatchMapper::insert); + voucher.setWaybillBatchNo(relations.stream().map(VoucherWaybillBatch::getWaybillBatchNo).distinct().collect(Collectors.joining(","))); + voucher.setRelatedWaybillCount(0); + voucher.setUnRelatedWaybillCount(0); + voucher.setAuditStatus("-"); + voucher.setRejectReason(""); + if (Func.isEmpty(voucher.getFileUrl())) { + // 文件尚未上传完成时只保存批次关系,待上传完成后再执行匹配。 + voucher.setProcessStatus("上传中"); + updateById(voucher); + return; + } + voucher.setProcessStatus("处理中"); + updateById(voucher); + + // 事务提交后重新读取凭证文件并按新批次执行车牌匹配。 + eventPublisher.publishEvent(new VoucherUploadCompletedEvent(voucher.getId())); + } + @Override @Transactional(rollbackFor = Exception.class) public void completeUploadFile(VoucherFileCompleteRequest request) { @@ -697,17 +886,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl> selectableWaybillBatches(IPage page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd) { - return voucherWaybillBatchMapper.selectVoucherWaybillBatchPage(page, AuthUtil.getTenantId(), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd); + return voucherWaybillBatchMapper.selectVoucherWaybillBatchPage(page, AuthUtil.getTenantId(), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd, + !AuthUtil.isAdministrator(), currentCarrierName()); } private List> selectableWaybillBatchesByIds(List ids) { if (Func.isEmpty(ids)) return List.of(); - return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids); + return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids, !AuthUtil.isAdministrator(), currentCarrierName()); + } + + /** + * 普通用户只能关联承运商名称与其当前组织名称一致的运单批次;超级管理员不受此限制。 + */ + private String currentCarrierName() { + if (AuthUtil.isAdministrator()) return null; + Long currentDeptId = Func.firstLong(AuthUtil.getDeptId()); + return getOrganizationName(currentDeptId); } private List listRelatedWaybills(VoucherManage voucher) { @@ -1029,6 +1230,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl 200) { + throw new ServiceException("驳回原因不能超过200字"); + } voucher.setAuditStatus("审核驳回"); + voucher.setRejectReason(Func.isEmpty(reason) ? "" : reason); updateById(voucher); log.info("凭证批次审核驳回 id={}, voucherBatchNo={}, operator={}, reason={}", - id, voucher.getVoucherBatchNo(), AuthUtil.getUserId(), rejectReason); + id, voucher.getVoucherBatchNo(), AuthUtil.getUserId(), reason); } /** diff --git a/doc/sql/transport/blade_voucher_manage_20260812.sql b/doc/sql/transport/blade_voucher_manage_20260812.sql index 071fb70..2ec6872 100644 --- a/doc/sql/transport/blade_voucher_manage_20260812.sql +++ b/doc/sql/transport/blade_voucher_manage_20260812.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `blade_voucher_manage` ( `related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '已关联运单数', `un_related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '未关联运单数', `audit_status` varchar(20) NOT NULL DEFAULT '-' COMMENT '待审核、审核通过、审核驳回', + `reject_reason` varchar(200) DEFAULT NULL COMMENT '审核驳回原因', `create_user` bigint DEFAULT NULL, `create_dept` bigint DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` tinyint NOT NULL DEFAULT 1, `is_deleted` tinyint NOT NULL DEFAULT 0, diff --git a/doc/sql/transport/blade_voucher_manage_reject_reason_20260909.sql b/doc/sql/transport/blade_voucher_manage_reject_reason_20260909.sql new file mode 100644 index 0000000..89d7014 --- /dev/null +++ b/doc/sql/transport/blade_voucher_manage_reject_reason_20260909.sql @@ -0,0 +1,25 @@ +-- 凭证管理补充审核驳回原因 +-- 日期:2026-09-09 + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS `upgrade_blade_voucher_manage_reject_reason_20260909`$$ +CREATE PROCEDURE `upgrade_blade_voucher_manage_reject_reason_20260909`() +BEGIN + DECLARE db_name varchar(128); + SET db_name = DATABASE(); + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = db_name + AND table_name = 'blade_voucher_manage' + AND column_name = 'reject_reason') THEN + ALTER TABLE `blade_voucher_manage` + ADD COLUMN `reject_reason` varchar(200) DEFAULT NULL COMMENT '审核驳回原因' + AFTER `audit_status`; + END IF; +END$$ + +CALL `upgrade_blade_voucher_manage_reject_reason_20260909`()$$ +DROP PROCEDURE `upgrade_blade_voucher_manage_reject_reason_20260909`$$ + +DELIMITER ; diff --git a/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql b/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql index 44cc8d0..76a4404 100644 --- a/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql +++ b/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql @@ -54,6 +54,9 @@ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'audit_status') THEN ALTER TABLE `blade_voucher_manage` ADD COLUMN `audit_status` varchar(20) NOT NULL DEFAULT '-' COMMENT '待审核、审核通过、审核驳回'; END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'reject_reason') THEN + ALTER TABLE `blade_voucher_manage` ADD COLUMN `reject_reason` varchar(200) DEFAULT NULL COMMENT '审核驳回原因'; + END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'create_user') THEN ALTER TABLE `blade_voucher_manage` ADD COLUMN `create_user` bigint DEFAULT NULL COMMENT '创建人'; END IF; From 93c7e6f8f5de134000a9d83cb8f32bb3a543d491 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 10 Sep 2026 22:50:00 +0800 Subject: [PATCH 087/114] =?UTF-8?q?1=E3=80=81=E8=B0=83=E6=95=B4=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E8=BF=90=E5=8D=95=202=E3=80=81=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=AF=B9=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springblade/system/feign/ISysClient.java | 11 + .../system/feign/ISysClientFallback.java | 5 + .../transport/pojo/entity/Driver.java | 5 + .../transport/pojo/entity/ProjectApply.java | 7 + .../pojo/entity/TransportVehicle.java | 5 + .../transport/pojo/vo/TransportVehicleVO.java | 7 + .../springblade/system/feign/IUserClient.java | 10 + .../springblade/system/feign/SysClient.java | 10 + .../springblade/system/feign/UserClient.java | 6 + .../TransportReconciliationController.java | 10 +- .../controller/WaybillController.java | 9 +- .../excel/CargoReconciliationExcel.java | 4 +- .../transport/excel/ProjectApplyExcel.java | 4 + .../ReconciliationImportTemplateExcel.java | 71 +++++++ .../excel/TransportVehicleExcel.java | 3 + .../excel/VehicleReconciliationExcel.java | 2 +- .../transport/mapper/DriverMapper.xml | 2 + .../mapper/TransportVehicleMapper.xml | 6 + .../mapper/WaybillImportBatchMapper.java | 11 + .../ITransportReconciliationService.java | 1 + .../service/IWaybillImportBatchService.java | 1 + .../service/impl/DriverServiceImpl.java | 191 +++++++++++++++++- .../service/impl/ProjectApplyServiceImpl.java | 8 + .../TransportReconciliationServiceImpl.java | 70 ++++++- .../impl/TransportVehicleServiceImpl.java | 82 +++++++- .../impl/WaybillImportBatchServiceImpl.java | 14 +- ...ply_business_mode_profit_rate_20260910.sql | 4 + .../blade_project_contract_management.sql | 2 + doc/sql/transport/blade_transport_driver.sql | 1 + ...blade_transport_driver_role_alias_note.sql | 6 + ...lade_transport_driver_user_id_20260910.sql | 3 + doc/sql/transport/blade_transport_vehicle.sql | 1 + ...nsport_vehicle_use_department_20260910.sql | 3 + 33 files changed, 553 insertions(+), 22 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ReconciliationImportTemplateExcel.java create mode 100644 doc/sql/transport/blade_project_apply_business_mode_profit_rate_20260910.sql create mode 100644 doc/sql/transport/blade_transport_driver_role_alias_note.sql create mode 100644 doc/sql/transport/blade_transport_driver_user_id_20260910.sql create mode 100644 doc/sql/transport/blade_transport_vehicle_use_department_20260910.sql diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java index fb18df7..37e33b1 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java @@ -64,6 +64,7 @@ public interface ISysClient { String ROLE_NAMES = API_PREFIX + "/role-names"; String ROLE_ALIAS = API_PREFIX + "/role-alias"; String ROLE_ALIASES = API_PREFIX + "/role-aliases"; + String ROLE_ID_BY_ALIAS = API_PREFIX + "/role-id-by-alias"; String TENANT = API_PREFIX + "/tenant"; String TENANT_ID = API_PREFIX + "/tenant-id"; String TENANT_PACKAGE = API_PREFIX + "/tenant-package"; @@ -240,6 +241,16 @@ public interface ISysClient { @GetMapping(ROLE_ALIASES) R> getRoleAliases(@RequestParam("roleIds") String roleIds); + /** + * 根据角色别名获取角色id + * + * @param tenantId 租户id + * @param roleAlias 角色别名 + * @return 角色id + */ + @GetMapping(ROLE_ID_BY_ALIAS) + R getRoleIdByAlias(@RequestParam("tenantId") String tenantId, @RequestParam("roleAlias") String roleAlias); + /** * 获取租户 * diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java index f31965c..c7598c3 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java @@ -129,6 +129,11 @@ public class ISysClientFallback implements ISysClient { return R.fail("获取数据失败"); } + @Override + public R getRoleIdByAlias(String tenantId, String roleAlias) { + return R.fail("获取数据失败"); + } + @Override public R getTenant(Long id) { return R.fail("获取数据失败"); diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java index 0ba8076..1d13e2e 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Driver.java @@ -191,6 +191,11 @@ public class Driver extends TenantEntity { */ @Schema(description = "手机号") private String mobile; + /** + * 关联系统用户ID + */ + @Schema(description = "关联系统用户ID") + private Long userId; /** * 与联系人关系 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java index be872cf..8ba12f4 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java @@ -118,6 +118,9 @@ public class ProjectApply extends TenantEntity { @Schema(description = "业务类型") private String businessType; + @Schema(description = "业务模式") + private String businessMode; + @Schema(description = "项目规模(万元)") @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) private BigDecimal projectScale; @@ -126,6 +129,10 @@ public class ProjectApply extends TenantEntity { @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) private BigDecimal estimatedProfit; + @Schema(description = "利润率(%)") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal profitRate; + @Schema(description = "资金需求(万元)") @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) private BigDecimal fundDemand; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java index e79e0ef..4829cc3 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportVehicle.java @@ -53,6 +53,11 @@ public class TransportVehicle extends TenantEntity { */ @Schema(description = "所属组织") private String organizationName; + /** + * 使用部门 + */ + @Schema(description = "使用部门") + private String useDepartment; /** * 车牌号 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java index 46acec1..7536d19 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java @@ -69,4 +69,11 @@ public class TransportVehicleVO extends TransportVehicle { @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate warningDate; + /** + * 绑定司机(按车牌反查司机驾驶车辆,多个用顿号拼接) + */ + @TableField(exist = false) + @Schema(description = "绑定司机") + private String boundDriver; + } diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java index 9dce693..cc78a95 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java @@ -57,6 +57,7 @@ public interface IUserClient { String USER_BY_ACCOUNT = API_PREFIX + "/user-by-account"; String USER_AUTH_INFO = API_PREFIX + "/user-auth-info"; String SAVE_USER = API_PREFIX + "/save-user"; + String UPDATE_USER = API_PREFIX + "/update-user"; String SAVE_IAM_USER = API_PREFIX + "/save-iam-user"; String REGISTER_USER = API_PREFIX + "/register-user"; String REMOVE_USER = API_PREFIX + "/remove-user"; @@ -150,6 +151,15 @@ public interface IUserClient { @PostMapping(SAVE_USER) R saveUser(@RequestBody User user); + /** + * 更新用户 + * + * @param user 用户实体 + * @return + */ + @PostMapping(UPDATE_USER) + R updateUser(@RequestBody User user); + /** * 新建IAM用户 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java index 789db31..49c8613 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java @@ -167,6 +167,16 @@ public class SysClient implements ISysClient { return R.data(roleService.getRoleAliases(roleIds)); } + @Override + @GetMapping(ROLE_ID_BY_ALIAS) + public R getRoleIdByAlias(String tenantId, String roleAlias) { + Role role = roleService.getOne(Wrappers.lambdaQuery() + .eq(Role::getTenantId, tenantId) + .eq(Role::getRoleAlias, roleAlias) + .last("LIMIT 1")); + return R.data(role == null || role.getId() == null ? null : String.valueOf(role.getId())); + } + @Override @GetMapping(TENANT) public R getTenant(Long id) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java index df55138..12b97fd 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java @@ -108,6 +108,12 @@ public class UserClient implements IUserClient { return R.data(service.submit(user)); } + @Override + @PostMapping(UPDATE_USER) + public R updateUser(@RequestBody User user) { + return R.data(service.updateUser(user)); + } + @Override @PostMapping(SAVE_IAM_USER) public R saveIamUser(@RequestBody User user) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java index b1f5b6b..2b5d777 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java @@ -23,6 +23,7 @@ import org.springblade.core.excel.util.ExcelUtil; import org.springblade.transport.excel.CargoReconciliationExcel; import org.springblade.transport.excel.CargoReconciliationFeeReader; import org.springblade.transport.excel.CargoReconciliationFailureExcel; +import org.springblade.transport.excel.ReconciliationImportTemplateExcel; import org.springblade.transport.excel.VehicleReconciliationExcel; import org.springblade.transport.excel.VehicleReconciliationFeeReader; import org.springblade.transport.excel.VehicleReconciliationFailureExcel; @@ -154,9 +155,12 @@ public class TransportReconciliationController extends BladeController { @GetMapping("/template") @ApiOperationSupport(order = 8) @Operation(summary = "下载运输对账模板") - public void template(@RequestParam String mode, HttpServletResponse response) { - if ("cargo".equals(mode)) ExcelUtil.export(response, "货物明细对账模板", "货物明细对账模板", new ArrayList(), CargoReconciliationExcel.class); - else ExcelUtil.export(response, "整车总额对账模板", "整车总额对账模板", new ArrayList(), VehicleReconciliationExcel.class); + public void template(@RequestParam String mode, @RequestParam(required = false) Long id, + @RequestParam(required = false) Long formalSettlementId, @RequestParam(required = false) String feeItems, + HttpServletResponse response) { + List extraFeeItems = reconciliationService.templateFeeItems(id, formalSettlementId, feeItems); + if ("cargo".equals(mode)) ReconciliationImportTemplateExcel.exportCargo(response, extraFeeItems); + else ReconciliationImportTemplateExcel.exportVehicle(response, extraFeeItems); } @PostMapping("/match") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index f7ffb9c..36c761b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -131,8 +131,15 @@ public class WaybillController extends BladeController { return R.data(options); } - @GetMapping("/import-batch/list") + @GetMapping("/import-batch/next-code") @ApiOperationSupport(order = 4) + @Operation(summary = "下一个运单批次号") + public R importBatchNextCode() { + return R.data(waybillImportBatchService.nextBatchNo()); + } + + @GetMapping("/import-batch/list") + @ApiOperationSupport(order = 5) @Operation(summary = "运单批量导入批次分页") public R> importBatchList(WaybillImportBatchRequest request, Query query) { return R.data(waybillImportBatchService.page(Condition.getPage(query), request)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java index 25a6e20..235be05 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java @@ -35,8 +35,8 @@ public class CargoReconciliationExcel implements Serializable { @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; @ExcelProperty("里程(KM)") @NumberFormat("0.00") private BigDecimal mileage; @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; - @ExcelProperty("水费") @NumberFormat("0.00") private BigDecimal feeItemOne; - @ExcelProperty("罚款") @NumberFormat("0.00") private BigDecimal feeItemTwo; + @ExcelIgnore @NumberFormat("0.00") private BigDecimal feeItemOne; + @ExcelIgnore @NumberFormat("0.00") private BigDecimal feeItemTwo; @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; @ExcelIgnore private Map feeItems; @ExcelIgnore private String errorMessage; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java index 25238f7..cf8bdaa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java @@ -86,10 +86,14 @@ public class ProjectApplyExcel implements Serializable { private String transportType; @ExcelProperty("业务类型") private String businessType; + @ExcelProperty("业务模式") + private String businessMode; @ExcelProperty("项目规模(万元)") private BigDecimal projectScale; @ExcelProperty("预计利润(万元)") private BigDecimal estimatedProfit; + @ExcelProperty("利润率(%)") + private BigDecimal profitRate; @ExcelProperty("资金需求(万元)") private BigDecimal fundDemand; @ExcelProperty("结算方式") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ReconciliationImportTemplateExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ReconciliationImportTemplateExcel.java new file mode 100644 index 0000000..9645b2d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ReconciliationImportTemplateExcel.java @@ -0,0 +1,71 @@ +/** + * 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.

+ *

Author: Chill Zhuang (bladejava@qq.com)

+ */ +package org.springblade.transport.excel; + +import cn.idev.excel.FastExcel; +import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** 运输对账导入模板导出。费用列按当前内部账单收费项动态生成。 */ +public final class ReconciliationImportTemplateExcel { + + private static final List VEHICLE_HEADERS = List.of( + "车牌号", "发货地址", "到货地址", "实际发货时间", "实际完成时间", "运输类型", + "货物名称", "货物类型", "运输总量", "里程(KM)", "批次号", "运输单价", "运输费" + ); + private static final List CARGO_HEADERS = List.of( + "车牌号", "发货地址", "到货地址", "实际发货时间", "实际完成时间", "货物名称", + "货物类型", "规格", "型号", "运输总量", "运输单价", "里程(KM)", "运输费" + ); + private static final String SETTLEMENT_AMOUNT = "结算费用合计"; + + private ReconciliationImportTemplateExcel() { + } + + public static void exportVehicle(HttpServletResponse response, List extraFeeItems) { + export(response, "整车总额对账模板", VEHICLE_HEADERS, extraFeeItems); + } + + public static void exportCargo(HttpServletResponse response, List extraFeeItems) { + export(response, "货物明细对账模板", CARGO_HEADERS, extraFeeItems); + } + + private static void export(HttpServletResponse response, String fileName, List baseHeaders, + List extraFeeItems) { + List> head = new ArrayList<>(); + for (String header : baseHeaders) { + head.add(List.of(header)); + } + if (extraFeeItems != null) { + for (String feeItem : extraFeeItems) { + if (feeItem != null && !feeItem.isBlank()) { + head.add(List.of(feeItem.trim())); + } + } + } + head.add(List.of(SETTLEMENT_AMOUNT)); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setHeader("Content-disposition", + "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) + ".xlsx"); + try { + FastExcel.write(response.getOutputStream()) + .head(head) + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .sheet(fileName) + .doWrite(List.of()); + } catch (IOException exception) { + throw new IllegalStateException("导出" + fileName + "失败", exception); + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/TransportVehicleExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/TransportVehicleExcel.java index 2bd98ee..ac5c02d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/TransportVehicleExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/TransportVehicleExcel.java @@ -59,6 +59,9 @@ public class TransportVehicleExcel implements Serializable { @ExcelProperty("所属组织 *") private String organizationName; + @ExcelProperty("使用部门") + private String useDepartment; + @ExcelProperty("业务关系 *") private String businessRelation; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java index f2d3266..9a214fa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java @@ -35,7 +35,7 @@ public class VehicleReconciliationExcel implements Serializable { @ExcelProperty("批次号") private String batchNo; @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; - @ExcelProperty("费用项目1") @NumberFormat("0.00") private BigDecimal feeItemOne; + @ExcelIgnore @NumberFormat("0.00") private BigDecimal feeItemOne; @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; @ExcelIgnore private Map feeItems; @ExcelIgnore private String errorMessage; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml index da48c1d..45ad067 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml @@ -40,6 +40,7 @@ + @@ -85,6 +86,7 @@ qualification_back, driver_type, mobile, + user_id, contact_relation, organization_name, emergency_contact_name, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml index 66e0907..c7411ab 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml @@ -13,6 +13,7 @@ + @@ -60,6 +61,7 @@ status, is_deleted, organization_name, + use_department, plate_no, plate_color, vehicle_type, @@ -116,6 +118,10 @@ AND organization_name LIKE #{organizationNameLike} + + + AND use_department LIKE #{useDepartmentLike} + AND plate_no LIKE #{plateNoLike} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java index 5631be8..61724ea 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillImportBatchMapper.java @@ -6,9 +6,20 @@ package org.springblade.transport.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.springblade.transport.pojo.entity.WaybillImportBatch; /** 运单批次 Mapper。 */ @Mapper public interface WaybillImportBatchMapper extends BaseMapper { + + /** + * 查询指定租户、指定日期前缀下已使用的最大批次流水号。 + * 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。 + */ + @Select("SELECT COALESCE(MAX(CAST(SUBSTRING(batch_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) " + + "FROM blade_waybill_import_batch WHERE tenant_id = #{tenantId} " + + "AND batch_no LIKE CONCAT(#{prefix}, '%')") + Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java index 1dc23ff..dc196cd 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java @@ -25,6 +25,7 @@ import java.util.List; public interface ITransportReconciliationService extends BaseService { IPage selectPage(IPage page, TransportReconciliationVO query); IPage formalOptions(IPage page, String settlementType, String keyword); + List templateFeeItems(Long id, Long formalSettlementId, String feeItems); TransportReconciliationVO detail(Long id); Long saveDraft(TransportReconciliationSaveRequest request); void removeDraft(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java index 36ccb57..a06dcd4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java @@ -15,4 +15,5 @@ public interface IWaybillImportBatchService extends BaseService page(IPage page, WaybillImportBatchRequest request); BusinessRemoveResultVO removeBatches(String ids); + String nextBatchNo(); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java index bf55063..ee887b7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java @@ -27,13 +27,25 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.feign.IUserClient; +import org.springblade.system.pojo.entity.User; +import org.springblade.system.pojo.entity.UserInfo; +import org.springblade.system.pojo.enums.UserSex; +import org.springblade.system.pojo.enums.UserType; import org.springblade.transport.excel.DriverExcel; import org.springblade.transport.mapper.DriverMapper; +import org.springblade.transport.mapper.TransportVehicleMapper; import org.springblade.transport.pojo.entity.Driver; +import org.springblade.transport.pojo.entity.TransportVehicle; import org.springblade.transport.pojo.vo.DriverExpiryStatVO; import org.springblade.transport.pojo.vo.DriverVO; import org.springblade.transport.service.IDriverService; @@ -50,6 +62,7 @@ import java.util.Objects; * @author Chill */ @Service +@RequiredArgsConstructor public class DriverServiceImpl extends BaseServiceImpl implements IDriverService { private static final int NAME_MAX_LENGTH = 10; @@ -62,6 +75,10 @@ public class DriverServiceImpl extends BaseServiceImpl imp private static final int DEFAULT_ENABLED_STATUS = 1; private static final int DEFAULT_FALSE = 0; + private final TransportVehicleMapper transportVehicleMapper; + private final IUserClient userClient; + private final ISysClient sysClient; + @Override public IPage selectDriverPage(IPage page, DriverVO driver) { prepareQuery(driver); @@ -74,7 +91,13 @@ public class DriverServiceImpl extends BaseServiceImpl imp prepare(driver); validate(driver); prepareSubmitTarget(driver); - return saveOrUpdate(driver); + // 编辑场景下字段无变化时 update 可能返回 false,不能据此跳过用户同步 + saveOrUpdate(driver); + if (Func.isEmpty(driver.getId())) { + throw new ServiceException("保存司机失败"); + } + syncDriverUser(driver); + return true; } @Override @@ -228,6 +251,14 @@ public class DriverServiceImpl extends BaseServiceImpl imp validateLength(driver.getMobile(), MOBILE_MAX_LENGTH, "手机号不能超过20字"); validateLength(driver.getEmergencyContactMobile(), MOBILE_MAX_LENGTH, "紧急联系人手机号不能超过20字"); validateLength(driver.getDrivingVehicle(), VEHICLE_NO_MAX_LENGTH, "驾驶车辆不能超过30字"); + if (Func.isNotEmpty(driver.getDrivingVehicle())) { + Long vehicleCount = transportVehicleMapper.selectCount(Wrappers.lambdaQuery() + .eq(TransportVehicle::getIsDeleted, 0) + .eq(TransportVehicle::getPlateNo, driver.getDrivingVehicle())); + if (vehicleCount == null || vehicleCount <= 0) { + throw new ServiceException("驾驶车辆不存在,请重新选择"); + } + } validateLength(driver.getDrivingLicenseNo(), SHORT_TEXT_MAX_LENGTH, "驾驶证档案编号不能超过50字"); validateLength(driver.getQualificationNo(), SHORT_TEXT_MAX_LENGTH, "资格证号不能超过50字"); validateLength(driver.getOrganizationName(), SHORT_TEXT_MAX_LENGTH, "所属组织不能超过50字"); @@ -273,6 +304,164 @@ public class DriverServiceImpl extends BaseServiceImpl imp } } + private void syncDriverUser(Driver driver) { + String tenantId = AuthUtil.getTenantId(); + if (Func.isEmpty(tenantId)) { + tenantId = driver.getTenantId(); + } + if (Func.isEmpty(tenantId)) { + throw new ServiceException("租户信息为空,无法同步系统用户"); + } + String mobile = driver.getMobile(); + if (Func.isEmpty(mobile) || mobile.length() < 6) { + throw new ServiceException("手机号长度不足,无法同步系统用户"); + } + // 编辑时前端可能不传 userId,从库中补齐已关联用户 + if (Func.isEmpty(driver.getUserId()) && Func.isNotEmpty(driver.getId())) { + Driver dbDriver = getById(driver.getId()); + if (dbDriver != null && Func.isNotEmpty(dbDriver.getUserId())) { + driver.setUserId(dbDriver.getUserId()); + } + } + + R roleResult = sysClient.getRoleIdByAlias(tenantId, "driver"); + if (roleResult == null || !R.isSuccess(roleResult) || Func.isEmpty(roleResult.getData())) { + throw new ServiceException("未配置角色别名为driver的角色,请先在系统角色中维护"); + } + String roleId = roleResult.getData(); + + R deptResult = sysClient.getDeptIds(tenantId, driver.getOrganizationName()); + if (deptResult == null || !R.isSuccess(deptResult) || Func.isEmpty(deptResult.getData())) { + throw new ServiceException("所属组织未匹配到系统部门,无法同步用户"); + } + String deptId = deptResult.getData(); + + User existing = findExistingUser(driver, tenantId, mobile); + if (existing == null || Func.isEmpty(existing.getId())) { + // 司机未关联有效用户时新建;密码为手机号后6位 + createAndLinkDriverUser(driver, tenantId, mobile, roleId, deptId); + } else { + updateAndLinkDriverUser(driver, existing, tenantId, mobile, roleId, deptId); + } + } + + private User findExistingUser(Driver driver, String tenantId, String mobile) { + if (Func.isNotEmpty(driver.getUserId())) { + R byId = userClient.userInfoById(driver.getUserId()); + if (byId != null && R.isSuccess(byId) && byId.getData() != null && Func.isNotEmpty(byId.getData().getId())) { + return byId.getData(); + } + // 关联的用户已不存在,清空后按手机号重建 + driver.setUserId(null); + } + return findExistingUserByAccountOrPhone(tenantId, mobile); + } + + private User findExistingUserByAccountOrPhone(String tenantId, String mobile) { + R byAccount = userClient.userByAccount(tenantId, mobile); + if (byAccount != null && R.isSuccess(byAccount) && byAccount.getData() != null + && Func.isNotEmpty(byAccount.getData().getId())) { + return byAccount.getData(); + } + R byPhone = userClient.userInfoByPhone(tenantId, mobile, UserType.WEB.getName()); + if (byPhone != null && R.isSuccess(byPhone) && byPhone.getData() != null + && byPhone.getData().getUser() != null && Func.isNotEmpty(byPhone.getData().getUser().getId())) { + return byPhone.getData().getUser(); + } + return null; + } + + private void createAndLinkDriverUser(Driver driver, String tenantId, String mobile, String roleId, String deptId) { + User user = buildSyncUser(null, tenantId, mobile, driver.getDriverName(), driver.getGender(), roleId, deptId); + user.setPassword(mobile.substring(mobile.length() - 6)); + user.setUserType(UserType.WEB.getCategory()); + + R saveResult = null; + try { + saveResult = userClient.saveUser(user); + } catch (Exception ex) { + User existed = findExistingUserByAccountOrPhone(tenantId, mobile); + if (existed != null) { + updateAndLinkDriverUser(driver, existed, tenantId, mobile, roleId, deptId); + return; + } + throw new ServiceException(Func.isNotEmpty(ex.getMessage()) ? ex.getMessage() : "同步系统用户失败"); + } + if (!isFeignSuccess(saveResult)) { + User existed = findExistingUserByAccountOrPhone(tenantId, mobile); + if (existed != null) { + updateAndLinkDriverUser(driver, existed, tenantId, mobile, roleId, deptId); + return; + } + throw new ServiceException(resolveFeignError(saveResult, "同步系统用户失败")); + } + + R created = userClient.userByAccount(tenantId, mobile); + User createdUser = (created != null && R.isSuccess(created)) ? created.getData() : null; + if (createdUser == null || Func.isEmpty(createdUser.getId())) { + createdUser = findExistingUserByAccountOrPhone(tenantId, mobile); + } + if (createdUser == null || Func.isEmpty(createdUser.getId())) { + throw new ServiceException("系统用户创建成功但无法回查用户ID"); + } + linkDriverUserId(driver, createdUser.getId()); + } + + private void updateAndLinkDriverUser(Driver driver, User existing, String tenantId, String mobile, String roleId, String deptId) { + User user = buildSyncUser(existing.getId(), tenantId, mobile, driver.getDriverName(), driver.getGender(), roleId, deptId); + R updateResult = userClient.updateUser(user); + // updateById 无字段变化时可能返回 false,但业务上仍视为成功,以接口 code 为准 + if (!isFeignSuccess(updateResult)) { + throw new ServiceException(resolveFeignError(updateResult, "同步更新系统用户失败")); + } + linkDriverUserId(driver, existing.getId()); + } + + private boolean isFeignSuccess(R result) { + return result != null && R.isSuccess(result); + } + + private String resolveFeignError(R result, String defaultMsg) { + if (result == null || Func.isEmpty(result.getMsg())) { + return defaultMsg; + } + String msg = result.getMsg().trim(); + // R.data(false) 也会带默认成功文案,不能当作业务错误抛出 + if ("操作成功".equals(msg) || "success".equalsIgnoreCase(msg)) { + return defaultMsg; + } + return msg; + } + + private User buildSyncUser(Long userId, String tenantId, String mobile, String driverName, String gender, + String roleId, String deptId) { + User user = new User(); + user.setId(userId); + user.setTenantId(tenantId); + user.setAccount(mobile); + user.setPhone(mobile); + user.setName(driverName); + user.setRealName(driverName); + user.setSex(UserSex.getCodeByName(gender)); + user.setRoleId(roleId); + user.setDeptId(deptId); + return user; + } + + private void linkDriverUserId(Driver driver, Long userId) { + if (Func.isEmpty(userId)) { + return; + } + if (Objects.equals(driver.getUserId(), userId)) { + return; + } + driver.setUserId(userId); + Driver patch = new Driver(); + patch.setId(driver.getId()); + patch.setUserId(userId); + updateById(patch); + } + private Long defaultZero(Long value) { return value == null ? 0L : value; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index 9dbbd54..08802b5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -348,6 +348,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl wrapper = Wrappers.lambdaQuery() - .eq(FormalSettlement::getApprovalStatus, "approved") + .eq(FormalSettlement::getApprovalStatus, FORMAL_DRAFT) .eq(Func.isNotEmpty(settlementType), FormalSettlement::getSettlementType, settlementType) .and(Func.isNotEmpty(keyword), value -> value.like(FormalSettlement::getFormalSettlementNo, keyword) .or().like(FormalSettlement::getContractNo, keyword).or().like(FormalSettlement::getContractName, keyword)); @@ -142,6 +144,60 @@ public class TransportReconciliationServiceImpl return formalSettlementMapper.selectPage(page, wrapper.orderByDesc(FormalSettlement::getCreateTime)); } + @Override + public List templateFeeItems(Long id, Long formalSettlementId, String feeItems) { + LinkedHashSet names = new LinkedHashSet<>(); + appendTemplateFeeItems(names, feeItems); + if (id != null) { + for (TransportReconciliationInternal row : internalRows(id)) { + parseFeeItems(row.getFeeItemsJson()).keySet().forEach(name -> appendTemplateFeeItem(names, name)); + } + TransportReconciliation bill = getById(id); + if (bill != null) collectFormalTemplateFeeItems(names, bill.getFormalSettlementId()); + } + collectFormalTemplateFeeItems(names, formalSettlementId); + return new ArrayList<>(names); + } + + private void appendTemplateFeeItems(Set names, String feeItems) { + if (Func.isEmpty(feeItems)) return; + for (String name : feeItems.split(",")) appendTemplateFeeItem(names, name); + } + + private void collectFormalTemplateFeeItems(Set names, Long formalSettlementId) { + if (formalSettlementId == null) return; + List summaries = formalSummaryFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId) + .eq(FormalSettlementSummaryFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementSummaryFee::getLineNo)); + for (FormalSettlementSummaryFee summary : summaries) { + appendTemplateFeeItem(names, summary.getFeeItem()); + } + List details = formalDetailMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, formalSettlementId) + .orderByAsc(FormalSettlementDetail::getLineNo)); + for (FormalSettlementDetail detail : details) { + parseFeeItems(detail.getFeeItemsJson()).keySet().forEach(name -> appendTemplateFeeItem(names, name)); + List fees = formalDetailFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()) + .eq(FormalSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetailFee::getLineNo)); + for (FormalSettlementDetailFee fee : fees) { + parseFeeItems(fee.getFeeItemsJson()).keySet().forEach(name -> appendTemplateFeeItem(names, name)); + } + } + } + + private void appendTemplateFeeItem(Set names, String name) { + if (Func.isEmpty(name)) return; + String trimmed = name.trim(); + if (trimmed.isEmpty() || isFreightFeeItem(trimmed) || "费用项目1".equals(trimmed)) return; + names.add(trimmed); + } + @Override public TransportReconciliationVO detail(Long id) { TransportReconciliationVO vo = TransportReconciliationWrapper.build().entityVO(existing(id)); @@ -166,7 +222,9 @@ public class TransportReconciliationServiceImpl throw new ServiceException("请选择正确的对账模式"); } FormalSettlement formal = formalSettlementMapper.selectById(request.getFormalSettlementId()); - if (formal == null || !"approved".equals(formal.getApprovalStatus())) throw new ServiceException("请选择已生效的正式结算单"); + if (formal == null || !FORMAL_DRAFT.equals(formal.getApprovalStatus())) { + throw new ServiceException("请选择草稿状态的正式结算单"); + } long occupied = count(Wrappers.lambdaQuery() .eq(TransportReconciliation::getFormalSettlementId, formal.getId()) .ne(request.getId() != null, TransportReconciliation::getId, request.getId())); @@ -394,9 +452,9 @@ public class TransportReconciliationServiceImpl external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); Map feeItems = row.getFeeItems(); if (feeItems == null || feeItems.isEmpty()) { - feeItems = new HashMap<>(); - feeItems.put("水费", money(row.getFeeItemOne())); - feeItems.put("罚款", money(row.getFeeItemTwo())); + feeItems = new LinkedHashMap<>(); + if (row.getFeeItemOne() != null) feeItems.put("水费", money(row.getFeeItemOne())); + if (row.getFeeItemTwo() != null) feeItems.put("罚款", money(row.getFeeItemTwo())); } external.setFeeItemsJson(JsonUtil.toJson(feeItems)); external.setMatchStatus(UNMATCHED); @@ -434,7 +492,7 @@ public class TransportReconciliationServiceImpl addImportDecimalErrors(validationErrors, row.getMileage(), "里程(KM)", 2); addImportDecimalErrors(validationErrors, row.getUnitPrice(), "运输单价", 2); addImportDecimalErrors(validationErrors, row.getFreightAmount(), "运输费", 2); - if (row.getFeeItems() != null) { + if (row.getFeeItems() != null && !row.getFeeItems().isEmpty()) { row.getFeeItems().forEach((name, value) -> addImportDecimalErrors(validationErrors, value, name, 2)); } else { addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "水费", 2); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java index 10d88ff..32462dc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java @@ -28,12 +28,15 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.transport.excel.TransportVehicleExcel; +import org.springblade.transport.mapper.DriverMapper; import org.springblade.transport.mapper.TransportVehicleMapper; +import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.pojo.entity.TransportVehicle; import org.springblade.transport.pojo.vo.TransportVehicleExpiryStatVO; import org.springblade.transport.pojo.vo.TransportVehicleVO; @@ -42,9 +45,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * 车辆管理 服务实现类 @@ -52,9 +59,13 @@ import java.util.regex.Pattern; * @author Chill */ @Service +@RequiredArgsConstructor public class TransportVehicleServiceImpl extends BaseServiceImpl implements ITransportVehicleService { private static final Pattern PLATE_NO_PATTERN = Pattern.compile("^[\\u4e00-\\u9fa5][A-Z][A-Z0-9挂学警港澳]{5,6}$"); + private static final String PLATE_COLOR_BLUE = "蓝牌"; + private static final String PLATE_COLOR_YELLOW = "黄牌"; + private static final String PLATE_COLOR_GREEN = "绿牌"; private static final int SHORT_TEXT_MAX_LENGTH = 50; private static final int ORG_MAX_LENGTH = 50; private static final int REMARK_MAX_LENGTH = 200; @@ -64,10 +75,55 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl selectTransportVehiclePage(IPage page, TransportVehicleVO vehicle) { prepareQuery(vehicle); - return page.setRecords(baseMapper.selectTransportVehiclePage(page, vehicle)); + List records = baseMapper.selectTransportVehiclePage(page, vehicle); + fillBoundDrivers(records); + return page.setRecords(records); + } + + /** + * 按车牌反查司机驾驶车辆,填充车辆列表「绑定司机」。 + */ + private void fillBoundDrivers(List records) { + if (Func.isEmpty(records)) { + return; + } + List plateNos = records.stream() + .map(TransportVehicleVO::getPlateNo) + .filter(Func::isNotEmpty) + .map(String::toUpperCase) + .distinct() + .toList(); + if (plateNos.isEmpty()) { + return; + } + List drivers = driverMapper.selectList(Wrappers.lambdaQuery() + .eq(Driver::getIsDeleted, 0) + .in(Driver::getDrivingVehicle, plateNos) + .select(Driver::getDriverName, Driver::getDrivingVehicle)); + Map> namesByPlate = new LinkedHashMap<>(); + for (Driver driver : drivers) { + if (Func.isEmpty(driver.getDrivingVehicle()) || Func.isEmpty(driver.getDriverName())) { + continue; + } + namesByPlate + .computeIfAbsent(driver.getDrivingVehicle().toUpperCase(), key -> new ArrayList<>()) + .add(driver.getDriverName()); + } + for (TransportVehicleVO record : records) { + if (Func.isEmpty(record.getPlateNo())) { + continue; + } + List names = namesByPlate.get(record.getPlateNo().toUpperCase()); + if (Func.isEmpty(names)) { + continue; + } + record.setBoundDriver(names.stream().distinct().collect(Collectors.joining("、"))); + } } @Override @@ -148,8 +204,10 @@ public class TransportVehicleServiceImpl extends BaseServiceImpllambdaQuery().likeRight(WaybillImportBatch::getBatchNo, prefix)); - return prefix + String.format("%04d", count + 1); + String prefix = "PC" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + Long maxSequence = baseMapper.selectMaxDailySequence(AuthUtil.getTenantId(), prefix); + return prefix + String.format("%04d", (maxSequence == null ? 0 : maxSequence) + 1); } /** diff --git a/doc/sql/transport/blade_project_apply_business_mode_profit_rate_20260910.sql b/doc/sql/transport/blade_project_apply_business_mode_profit_rate_20260910.sql new file mode 100644 index 0000000..eee711b --- /dev/null +++ b/doc/sql/transport/blade_project_apply_business_mode_profit_rate_20260910.sql @@ -0,0 +1,4 @@ +-- 项目立项增加业务模式、利润率 +ALTER TABLE `blade_project_apply` + ADD COLUMN `business_mode` varchar(50) DEFAULT NULL COMMENT '业务模式' AFTER `business_type`, + ADD COLUMN `profit_rate` decimal(18,2) DEFAULT NULL COMMENT '利润率(%)' AFTER `estimated_profit`; diff --git a/doc/sql/transport/blade_project_contract_management.sql b/doc/sql/transport/blade_project_contract_management.sql index 2f2d194..07920d2 100644 --- a/doc/sql/transport/blade_project_contract_management.sql +++ b/doc/sql/transport/blade_project_contract_management.sql @@ -33,8 +33,10 @@ CREATE TABLE `blade_project_apply` ( `transport_route` varchar(200) DEFAULT NULL COMMENT '运输线路', `transport_type` varchar(100) DEFAULT NULL COMMENT '运输类型', `business_type` varchar(100) DEFAULT NULL COMMENT '业务类型', + `business_mode` varchar(50) DEFAULT NULL COMMENT '业务模式', `project_scale` decimal(18,2) DEFAULT NULL COMMENT '项目规模(万元)', `estimated_profit` decimal(18,2) DEFAULT NULL COMMENT '预计利润(万元)', + `profit_rate` decimal(18,2) DEFAULT NULL COMMENT '利润率(%)', `fund_demand` decimal(18,2) DEFAULT NULL COMMENT '资金需求(万元)', `settlement_mode` varchar(100) DEFAULT NULL COMMENT '结算方式', `handler_user_id` bigint(20) DEFAULT NULL COMMENT '项目经办人ID', diff --git a/doc/sql/transport/blade_transport_driver.sql b/doc/sql/transport/blade_transport_driver.sql index 0b426a5..57ccd4f 100644 --- a/doc/sql/transport/blade_transport_driver.sql +++ b/doc/sql/transport/blade_transport_driver.sql @@ -33,6 +33,7 @@ CREATE TABLE `blade_transport_driver` ( `qualification_back` varchar(1000) DEFAULT NULL COMMENT '从业资格证内容页', `driver_type` varchar(20) NOT NULL COMMENT '司机类型:自有/外协', `mobile` varchar(20) NOT NULL COMMENT '手机号', + `user_id` bigint(20) DEFAULT NULL COMMENT '关联系统用户ID', `contact_relation` varchar(20) DEFAULT NULL COMMENT '与联系人关系', `organization_name` varchar(50) NOT NULL COMMENT '所属组织', `emergency_contact_name` varchar(20) NOT NULL COMMENT '紧急联系人姓名', diff --git a/doc/sql/transport/blade_transport_driver_role_alias_note.sql b/doc/sql/transport/blade_transport_driver_role_alias_note.sql new file mode 100644 index 0000000..31bf863 --- /dev/null +++ b/doc/sql/transport/blade_transport_driver_role_alias_note.sql @@ -0,0 +1,6 @@ +-- 司机同步系统用户依赖角色别名 role_alias = 'driver' +-- 请在系统管理 -> 角色管理中为对应租户新增/维护角色,并设置角色别名为 driver +-- 参考 blade_role 表结构:id, tenant_id, parent_id, role_name, sort, role_alias, status, is_deleted +-- 示例(请按实际租户与主键策略自行调整,勿直接照抄执行): +-- INSERT INTO blade_role (id, tenant_id, parent_id, role_name, sort, role_alias, status, is_deleted) +-- VALUES (你的雪花ID, '你的租户ID', 0, '司机', 10, 'driver', 1, 0); diff --git a/doc/sql/transport/blade_transport_driver_user_id_20260910.sql b/doc/sql/transport/blade_transport_driver_user_id_20260910.sql new file mode 100644 index 0000000..5d85fee --- /dev/null +++ b/doc/sql/transport/blade_transport_driver_user_id_20260910.sql @@ -0,0 +1,3 @@ +-- 司机表增加关联系统用户ID +ALTER TABLE `blade_transport_driver` + ADD COLUMN `user_id` bigint(20) DEFAULT NULL COMMENT '关联系统用户ID' AFTER `mobile`; diff --git a/doc/sql/transport/blade_transport_vehicle.sql b/doc/sql/transport/blade_transport_vehicle.sql index b884f22..a5260fb 100644 --- a/doc/sql/transport/blade_transport_vehicle.sql +++ b/doc/sql/transport/blade_transport_vehicle.sql @@ -6,6 +6,7 @@ CREATE TABLE `blade_transport_vehicle` ( `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID', `organization_name` varchar(50) NOT NULL COMMENT '所属组织', + `use_department` varchar(50) DEFAULT NULL COMMENT '使用部门', `plate_no` varchar(16) NOT NULL COMMENT '车牌号', `plate_color` varchar(20) DEFAULT NULL COMMENT '车牌颜色', `vehicle_type` varchar(50) NOT NULL COMMENT '车辆类型', diff --git a/doc/sql/transport/blade_transport_vehicle_use_department_20260910.sql b/doc/sql/transport/blade_transport_vehicle_use_department_20260910.sql new file mode 100644 index 0000000..c0dabca --- /dev/null +++ b/doc/sql/transport/blade_transport_vehicle_use_department_20260910.sql @@ -0,0 +1,3 @@ +-- 车辆表增加使用部门 +ALTER TABLE `blade_transport_vehicle` + ADD COLUMN `use_department` varchar(50) DEFAULT NULL COMMENT '使用部门' AFTER `organization_name`; From aa8b7ade99b186b10684008325d15cf13e43f040 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 11 Sep 2026 14:39:10 +0800 Subject: [PATCH 088/114] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/dto/EnrouteSubmitDTO.java | 42 + .../transport/pojo/dto/NodeSubmitDTO.java | 61 + .../transport/pojo/entity/Waybill.java | 23 + .../pojo/entity/WaybillEnroutePunch.java | 53 + .../pojo/entity/WaybillNodePunch.java | 74 ++ .../pojo/vo/DriverEnrouteRecordVO.java | 32 + .../transport/pojo/vo/DriverNodePunchVO.java | 52 + .../transport/pojo/vo/DriverPunchNodeVO.java | 79 ++ .../transport/pojo/vo/DriverPunchPhotoVO.java | 32 + .../pojo/vo/DriverVehicleCardVO.java | 74 ++ .../pojo/vo/DriverWaybillCardVO.java | 136 +++ .../pojo/vo/DriverWaybillPreviewVO.java | 49 + .../pojo/vo/DriverWaybillTabCountsVO.java | 55 + .../pojo/vo/ExceptionDisposalVO.java | 8 + .../transport/pojo/vo/ProcessConfigVO.java | 4 + .../pojo/vo/WaybillPunchPhotoVO.java | 38 + .../pojo/vo/WaybillPunchRecordItemVO.java | 73 ++ .../pojo/vo/WaybillPunchRecordsVO.java | 31 + .../transport/pojo/vo/WaybillVO.java | 4 + .../controller/DriverAppController.java | 69 ++ .../controller/DriverWaybillController.java | 156 +++ .../ExceptionDisposalController.java | 26 +- .../controller/WaybillController.java | 15 +- .../mapper/WaybillEnroutePunchMapper.java | 16 + .../mapper/WaybillNodePunchMapper.java | 16 + .../transport/service/IDriverAppService.java | 47 + .../service/IDriverWaybillService.java | 96 ++ .../service/IExceptionDisposalService.java | 5 + .../transport/service/IWaybillService.java | 17 +- .../service/impl/DriverAppServiceImpl.java | 210 ++++ .../impl/DriverWaybillServiceImpl.java | 1069 +++++++++++++++++ .../impl/ExceptionDisposalServiceImpl.java | 125 +- .../impl/ProcessConfigServiceImpl.java | 79 +- .../service/impl/WaybillServiceImpl.java | 480 +++++++- .../support/WaybillProcessSupport.java | 472 ++++++++ .../transport/wrapper/WaybillWrapper.java | 7 +- doc/sql/transport/blade_tms_business.sql | 5 + .../blade_waybill_driver_accept_20260911.sql | 6 + .../blade_waybill_enroute_punch_20260911.sql | 23 + .../blade_waybill_node_punch_20260911.sql | 30 + 40 files changed, 3850 insertions(+), 39 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/EnrouteSubmitDTO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillEnroutePunch.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillNodePunch.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnrouteRecordVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchPhotoVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVehicleCardVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillPreviewVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchPhotoVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordsVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillEnroutePunchMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillNodePunchMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java create mode 100644 doc/sql/transport/blade_waybill_driver_accept_20260911.sql create mode 100644 doc/sql/transport/blade_waybill_enroute_punch_20260911.sql create mode 100644 doc/sql/transport/blade_waybill_node_punch_20260911.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/EnrouteSubmitDTO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/EnrouteSubmitDTO.java new file mode 100644 index 0000000..309b921 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/EnrouteSubmitDTO.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 在途打卡提交 + */ +@Data +@Schema(description = "在途打卡提交") +public class EnrouteSubmitDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long waybillId; + + @Schema(description = "定位信息") + private Location location; + + @Schema(description = "货物照片URL") + private String photo; + + @Data + @Schema(description = "定位") + public static class Location implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private Double longitude; + private Double latitude; + private String address; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java new file mode 100644 index 0000000..6b16994 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 过程节点打卡提交(到场/装货/卸货/签收等,不含在途) + */ +@Data +@Schema(description = "过程节点打卡提交") +public class NodeSubmitDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long waybillId; + + @Schema(description = "过程节点 key", requiredMode = Schema.RequiredMode.REQUIRED) + private String nodeCode; + + @Schema(description = "定位信息") + private Location location; + + @Schema(description = "凭证照片 URL 列表") + private List photos; + + @Schema(description = "重量(吨)") + private String weight; + + @Schema(description = "体积(方)") + private String volume; + + @Schema(description = "数量(件)") + private String quantity; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否异常") + private Boolean exception; + + @Data + @Schema(description = "定位") + public static class Location implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private Double longitude; + private Double latitude; + private String address; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index aaa207b..81c481d 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -22,6 +22,8 @@ */ package org.springblade.transport.pojo.entity; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -31,6 +33,7 @@ import org.springblade.core.tenant.mp.TenantEntity; import java.io.Serial; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.Date; /** * 运单管理实体类 @@ -139,6 +142,26 @@ public class Waybill extends TenantEntity { @Schema(description = "司机手机号") private String driverPhone; + @Schema(description = "司机接单状态:pending待接单/accepted已接单/rejected已拒绝") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String driverAcceptStatus; + + @Schema(description = "司机接单时间") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Date driverAcceptTime; + + @Schema(description = "接单司机ID") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Long driverAcceptDriverId; + + @Schema(description = "司机拒绝接单时间") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Date driverRejectTime; + + @Schema(description = "司机拒绝接单原因") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String driverRejectReason; + @Schema(description = "车/船/航班/班列号") private String vehicleNo; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillEnroutePunch.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillEnroutePunch.java new file mode 100644 index 0000000..5c01db0 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillEnroutePunch.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 运单在途打卡记录 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_waybill_enroute_punch") +@Schema(description = "运单在途打卡记录") +public class WaybillEnroutePunch extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "打卡司机ID") + private Long driverId; + + @Schema(description = "打卡时间") + private Date punchTime; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "打卡地址") + private String address; + + @Schema(description = "货物照片URL") + private String photo; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillNodePunch.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillNodePunch.java new file mode 100644 index 0000000..6c2cc15 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/WaybillNodePunch.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 运单过程节点打卡记录 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_waybill_node_punch") +@Schema(description = "运单过程节点打卡记录") +public class WaybillNodePunch extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "打卡司机ID") + private Long driverId; + + @Schema(description = "过程节点 key") + private String nodeCode; + + @Schema(description = "过程节点名称") + private String nodeName; + + @Schema(description = "打卡时间") + private Date punchTime; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "打卡地址") + private String address; + + @Schema(description = "凭证照片URL,多张逗号分隔") + private String photos; + + @Schema(description = "重量(吨)") + private String weight; + + @Schema(description = "体积(方)") + private String volume; + + @Schema(description = "数量(件)") + private String quantity; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否异常:0否 1是") + private Integer exceptionFlag; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnrouteRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnrouteRecordVO.java new file mode 100644 index 0000000..73351e6 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverEnrouteRecordVO.java @@ -0,0 +1,32 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 司机端在途打卡记录 + */ +@Data +@Schema(description = "司机端在途打卡记录") +public class DriverEnrouteRecordVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "打卡时间 HH:mm 或 yyyy-MM-dd HH:mm:ss") + private String time; + + @Schema(description = "打卡地址") + private String address; + + @Schema(description = "货物照片") + private String photo; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java new file mode 100644 index 0000000..4bca631 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 司机端过程节点打卡结果 + */ +@Data +@Schema(description = "司机端过程节点打卡结果") +public class DriverNodePunchVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "节点 key") + private String nodeCode; + + @Schema(description = "节点名称") + private String nodeName; + + @Schema(description = "打卡时间(ISO 或 yyyy-MM-dd HH:mm:ss)") + private String checkinTime; + + @Schema(description = "打卡地址") + private String address; + + @Schema(description = "凭证照片") + private List photos = new ArrayList<>(); + + @Schema(description = "重量") + private String weight; + + @Schema(description = "体积") + private String volume; + + @Schema(description = "数量") + private String quantity; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java new file mode 100644 index 0000000..f7e8882 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java @@ -0,0 +1,79 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 司机端过程打卡节点(过程配置 punch=是) + */ +@Data +@Schema(description = "司机端过程打卡节点") +public class DriverPunchNodeVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "节点 key,如 arrive_scene / load / transit") + private String key; + + @Schema(description = "节点名称") + private String name; + + @Schema(description = "是否在途节点") + private Boolean transit; + + @Schema(description = "是否已打卡(在途=今日已打)") + private Boolean done; + + @Schema(description = "当前是否可打卡(顺序门禁 + 在途频次时段)") + private Boolean actionable; + + @Schema(description = "是否展示该卡(在途可能因频次/时段隐藏)") + private Boolean visible; + + @Schema(description = "是否默认展开(仅第一个可打卡节点)") + private Boolean defaultExpanded; + + @Schema(description = "打卡时间展示") + private String checkinTime; + + @Schema(description = "打卡地点") + private String checkinPlace; + + @Schema(description = "重量(吨)") + private String weight; + + @Schema(description = "体积(方)") + private String volume; + + @Schema(description = "数量(件)") + private String quantity; + + @Schema(description = "已上传凭证图(已打卡回显)") + private List photos = new ArrayList<>(); + + @Schema(description = "是否需要定位") + private Boolean needLocation; + + @Schema(description = "是否需要上传货量") + private Boolean needCargo; + + @Schema(description = "货量类型:重量/体积/数量") + private List cargoTypes = new ArrayList<>(); + + @Schema(description = "是否需要上传凭证") + private Boolean needVoucher; + + @Schema(description = "凭证类型") + private List voucherTypes = new ArrayList<>(); + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchPhotoVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchPhotoVO.java new file mode 100644 index 0000000..bbd9173 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchPhotoVO.java @@ -0,0 +1,32 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 司机端打卡凭证图 + */ +@Data +@Schema(description = "司机端打卡凭证图") +public class DriverPunchPhotoVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "凭证类型,如 委托单") + private String type; + + @Schema(description = "展示标签,如 装货-委托单") + private String label; + + @Schema(description = "图片 URL") + private String url; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVehicleCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVehicleCardVO.java new file mode 100644 index 0000000..46bbfc7 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVehicleCardVO.java @@ -0,0 +1,74 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 司机端车辆卡片(对齐小程序 VehicleAuthInfo) + */ +@Data +@Schema(description = "司机端车辆卡片") +public class DriverVehicleCardVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "车辆ID") + private Long id; + + @Schema(description = "车牌号") + private String plateNo; + + @Schema(description = "车辆类型") + private String vehicleType; + + @Schema(description = "行驶证主页照片") + private String licenseFrontUrl; + + @Schema(description = "行驶证副页照片") + private String licenseBackUrl; + + @Schema(description = "道路运输证号") + private String roadTransportNo; + + @Schema(description = "道路运输证照片") + private String roadTransportUrl; + + @Schema(description = "车架号") + private String vin; + + @Schema(description = "发动机号") + private String engineNo; + + @Schema(description = "行驶证有效期止") + private String licenseValidEnd; + + @Schema(description = "认证状态:0认证中 1认证通过 2认证驳回") + private Integer certificationStatus; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java new file mode 100644 index 0000000..1b87f5d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java @@ -0,0 +1,136 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 司机端运单卡片(首页当前任务 / 待接预览 / 列表项) + *

+ * 字段对齐小程序 MockWaybillItem,status 为数字枚举: + * 0 待接单 / 1 运输中 / 2 已完成 / 3 已取消 + */ +@Data +@Schema(description = "司机端运单卡片") +public class DriverWaybillCardVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long id; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "起点名称") + private String fromName; + + @Schema(description = "终点名称") + private String toName; + + @Schema(description = "起点地址") + private String fromAddress; + + @Schema(description = "终点地址") + private String toAddress; + + @Schema(description = "货物名称列表") + private List cargoNames; + + @Schema(description = "货物类别") + private String cargoCategory; + + @Schema(description = "重量(带单位)") + private String weight; + + @Schema(description = "状态:0待接单/1运输中/2已完成/3已取消") + private Integer status; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "发布时间(兼容小程序 publishTime)") + private String publishTime; + + @Schema(description = "当前过程节点") + private String currentNode; + + @Schema(description = "计划/完成时间段展示") + private String timeRange; + + @Schema(description = "运费参考金额") + private BigDecimal freight; + + @Schema(description = "司机姓名") + private String driverName; + + @Schema(description = "司机手机号") + private String driverPhone; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "是否需要司机确认接单") + private Boolean requireAccept; + + @Schema(description = "司机接单状态:pending待接单/accepted已接单/rejected已拒绝") + private String acceptStatus; + + @Schema(description = "司机拒绝接单原因") + private String rejectReason; + + @Schema(description = "过程配置是否启用在途打卡(在途节点 punch=是)") + private Boolean transitPunchEnabled; + + @Schema(description = "是否展示「今日在途打卡」面板(到期且在时段内,或今日已打)") + private Boolean transitCheckinVisible; + + @Schema(description = "今日是否需要在途打卡(到期且未打且在时段内)") + private Boolean requireTransitCheckinToday; + + @Schema(description = "今日是否已完成在途打卡") + private Boolean transitCheckinDoneToday; + + @Schema(description = "在途打卡频次(每 N 天 1 次)") + private Integer transitFrequencyDays; + + @Schema(description = "在途打卡时段开始 HH:mm") + private String transitTimeStart; + + @Schema(description = "在途打卡时段结束 HH:mm") + private String transitTimeEnd; + + @Schema(description = "在途打卡记录(详情返回)") + private List enrouteRecords; + + @Schema(description = "过程配置中 punch=是 的打卡节点列表(详情返回)") + private List punchNodes; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillPreviewVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillPreviewVO.java new file mode 100644 index 0000000..a4e6158 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillPreviewVO.java @@ -0,0 +1,49 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 司机端待接运单预览 + */ +@Data +@Schema(description = "司机端待接运单预览") +public class DriverWaybillPreviewVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预览列表") + private List records = new ArrayList<>(); + + @Schema(description = "待接运单总数(角标)") + private Long total = 0L; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java new file mode 100644 index 0000000..3aa95f5 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java @@ -0,0 +1,55 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 司机端运单列表 Tab 统计 + *

+ * 对齐小程序 { all, pending, doing, done } + */ +@Data +@Schema(description = "司机端运单 Tab 统计") +public class DriverWaybillTabCountsVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "全部(待接单+运输中+已完成)") + private long all; + + @Schema(description = "待接单") + private long pending; + + @Schema(description = "进行中(运输中)") + private long doing; + + @Schema(description = "已完成") + private long done; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ExceptionDisposalVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ExceptionDisposalVO.java index 2cb7fb8..7783903 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ExceptionDisposalVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ExceptionDisposalVO.java @@ -85,4 +85,12 @@ public class ExceptionDisposalVO extends ExceptionDisposal { @Schema(description = "扩展信息") private Map extra; + @TableField(exist = false) + @Schema(description = "运单路线:{start, end}") + private Map route; + + @TableField(exist = false) + @Schema(description = "货物信息:{name, weight}") + private Map cargo; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java index 424a568..1a3facb 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java @@ -66,4 +66,8 @@ public class ProcessConfigVO extends ProcessConfig { @Schema(description = "当前运单是否有关联凭证") private Boolean hasRelatedVoucher; + @TableField(exist = false) + @Schema(description = "关联项目是否已有运单") + private Boolean hasRelatedWaybill; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchPhotoVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchPhotoVO.java new file mode 100644 index 0000000..e752b28 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchPhotoVO.java @@ -0,0 +1,38 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 司机打卡上传图片(节点-凭证类型) + */ +@Data +@Schema(description = "司机打卡上传图片") +public class WaybillPunchPhotoVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "展示标签,如 装货-委托单") + private String label; + + @Schema(description = "图片 URL") + private String url; + + @Schema(description = "节点名称") + private String nodeName; + + @Schema(description = "凭证类型") + private String voucherType; + + @Schema(description = "打卡时间") + private String punchTime; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java new file mode 100644 index 0000000..140d6bc --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 管理端单条打卡记录 + */ +@Data +@Schema(description = "管理端单条打卡记录") +public class WaybillPunchRecordItemVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "记录ID") + private Long id; + + @Schema(description = "类型:node / enroute") + private String type; + + @Schema(description = "节点 key") + private String nodeCode; + + @Schema(description = "节点名称") + private String nodeName; + + @Schema(description = "打卡时间") + private String punchTime; + + @Schema(description = "打卡地址") + private String address; + + @Schema(description = "经度") + private String longitude; + + @Schema(description = "纬度") + private String latitude; + + @Schema(description = "重量") + private String weight; + + @Schema(description = "体积") + private String volume; + + @Schema(description = "数量") + private String quantity; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否异常") + private Boolean exceptionFlag; + + @Schema(description = "是否已打卡") + private Boolean punched; + + @Schema(description = "状态文案:已打卡/未打卡") + private String statusName; + + @Schema(description = "本条打卡凭证图") + private List photos = new ArrayList<>(); + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordsVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordsVO.java new file mode 100644 index 0000000..85a5394 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordsVO.java @@ -0,0 +1,31 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 管理端运单打卡记录汇总 + */ +@Data +@Schema(description = "管理端运单打卡记录汇总") +public class WaybillPunchRecordsVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "打卡流水(按时间升序,含节点/在途)") + private List records = new ArrayList<>(); + + @Schema(description = "司机上传凭证图(扁平列表,label=节点-凭证类型)") + private List driverUploads = new ArrayList<>(); + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java index 915d715..cea37c3 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java @@ -66,6 +66,10 @@ public class WaybillVO extends Waybill { @Schema(description = "业务状态名称") private String businessStatusName; + @TableField(exist = false) + @Schema(description = "是否需要司机确认接单(过程配置接单节点)") + private Boolean requireAccept; + @TableField(exist = false) @Schema(description = "是否仅查询未配载运单") private Integer onlyUnassignedLoading; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java new file mode 100644 index 0000000..6040145 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverAppController.java @@ -0,0 +1,69 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.controller; + +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.vo.DriverVehicleCardVO; +import org.springblade.transport.pojo.vo.DriverVO; +import org.springblade.transport.service.IDriverAppService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 司机端档案(小程序) + *

+ * 对外路径:{@code /api/blade-transport/driver/**} + * 同时兼容未去前缀直连 {@code /blade-transport/driver/**}。 + */ +@RestController +@AllArgsConstructor +@RequestMapping({"/driver", "/blade-transport/driver"}) +@Tag(name = "司机端档案", description = "小程序司机个人档案") +public class DriverAppController extends BladeController { + + private final IDriverAppService driverAppService; + + @GetMapping("/mine") + @ApiOperationSupport(order = 1) + @Operation(summary = "当前登录司机档案", description = "按手机号匹配 blade_transport_driver.mobile;可传 mobile,未传则从登录态解析") + public R mine(@RequestParam(required = false) String mobile) { + return R.data(driverAppService.currentByPhone(mobile)); + } + + @GetMapping("/vehicles") + @ApiOperationSupport(order = 2) + @Operation(summary = "当前司机车辆列表", description = "按司机 driving_vehicle 车牌匹配 blade_transport_vehicle") + public R> vehicles() { + return R.data(driverAppService.myVehicles()); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java new file mode 100644 index 0000000..3605085 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java @@ -0,0 +1,156 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.pojo.dto.EnrouteSubmitDTO; +import org.springblade.transport.pojo.dto.NodeSubmitDTO; +import org.springblade.transport.pojo.vo.DriverEnrouteRecordVO; +import org.springblade.transport.pojo.vo.DriverNodePunchVO; +import org.springblade.transport.pojo.vo.DriverWaybillCardVO; +import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO; +import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO; +import org.springblade.transport.service.IDriverWaybillService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 司机端运单接口(小程序) + *

+ * 对外完整路径:{@code /api/blade-transport/waybill/**} + * (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /waybill/**})。 + * 同时兼容未去前缀直连({@code /blade-transport/waybill/**}),避免 404。 + * 不挂管理端菜单鉴权,仅需登录态(Blade Secure)。 + */ +@RestController +@AllArgsConstructor +@RequestMapping({"/waybill", "/blade-transport/waybill"}) +@Tag(name = "司机端运单", description = "小程序司机端运单") +public class DriverWaybillController extends BladeController { + + private final IDriverWaybillService driverWaybillService; + + @GetMapping("/current-task") + @ApiOperationSupport(order = 1) + @Operation(summary = "首页:当前运输中任务", description = "当前司机绑定车牌下 businessStatus=running 的最新一条运单") + public R currentTask() { + return R.data(driverWaybillService.currentTask()); + } + + @GetMapping("/pending-preview") + @ApiOperationSupport(order = 2) + @Operation(summary = "首页:待接运单预览", description = "当前司机绑定车牌下 businessStatus=pending 的预览列表与总数") + public R pendingPreview( + @Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) { + return R.data(driverWaybillService.pendingPreview(size)); + } + + @GetMapping("/counts") + @ApiOperationSupport(order = 3) + @Operation(summary = "运单 Tab 统计", description = "仅统计当前司机绑定车牌对应的运单:全部 / 待接单 / 进行中 / 已完成") + public R counts() { + return R.data(driverWaybillService.tabCounts()); + } + + @GetMapping("/page") + @ApiOperationSupport(order = 4) + @Operation(summary = "运单分页列表", description = "仅返回当前司机绑定车牌(driving_vehicle)匹配运单 vehicleNo/trailerVehicleNo 的数据;status:空=全部,0待接单,1运输中,2已完成") + public R> page( + @Parameter(description = "当前页") @RequestParam(required = false) Integer current, + @Parameter(description = "每页条数") @RequestParam(required = false) Integer size, + @Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status, + @Parameter(description = "关键字:运单号/起终点") @RequestParam(required = false) String keyword) { + Integer statusCode = parseStatus(status); + return R.data(driverWaybillService.page(current, size, statusCode, keyword)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 5) + @Operation(summary = "司机运单详情", description = "返回 requireAccept、在途打卡可见性(transitCheckinVisible / requireTransitCheckinToday)等字段") + public R detail( + @Parameter(description = "运单ID", required = true) @RequestParam Long id) { + return R.data(driverWaybillService.detail(id)); + } + + @PostMapping("/accept") + @ApiOperationSupport(order = 6) + @Operation(summary = "司机确认接单", description = "过程配置接单为「是」时,司机确认接单后运单进入进行中") + public R accept(@Parameter(description = "运单ID", required = true) @RequestParam Long id) { + return R.status(driverWaybillService.accept(id)); + } + + @PostMapping("/reject") + @ApiOperationSupport(order = 7) + @Operation(summary = "司机拒绝接单", description = "过程配置接单为「是」时,司机可拒绝接单,运单保持待执行并记录拒单") + public R reject( + @Parameter(description = "运单ID", required = true) @RequestParam Long id, + @Parameter(description = "拒绝原因") @RequestParam(required = false) String reason) { + return R.status(driverWaybillService.reject(id, reason)); + } + + @PostMapping("/enroute/submit") + @ApiOperationSupport(order = 8) + @Operation(summary = "提交在途打卡", description = "过程配置在途节点 punch=是,且满足频次/时段时允许提交") + public R submitEnroute(@RequestBody EnrouteSubmitDTO dto) { + return R.data(driverWaybillService.submitEnroute(dto)); + } + + @PostMapping("/node/submit") + @ApiOperationSupport(order = 9) + @Operation(summary = "提交过程节点打卡", description = "到场/装货/发货/到货/卸货/签收等 punch=是;在途请走 /enroute/submit") + public R submitNode(@RequestBody NodeSubmitDTO dto) { + return R.data(driverWaybillService.submitNode(dto)); + } + + @PostMapping("/complete") + @ApiOperationSupport(order = 10) + @Operation(summary = "司机完成运单", description = "校验司机归属后改状态为已完成,并检查生成应收应付明细(与管理端一致)") + public R complete(@Parameter(description = "运单ID", required = true) @RequestParam Long id) { + return R.status(driverWaybillService.complete(id)); + } + + /** 前端可能传空字符串表示「全部」 */ + private Integer parseStatus(String status) { + if (Func.isEmpty(status)) { + return null; + } + try { + return Integer.valueOf(status.trim()); + } catch (NumberFormatException ex) { + return null; + } + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java index f039e69..3a64704 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java @@ -33,6 +33,7 @@ import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest; +import org.springblade.transport.pojo.entity.ExceptionDisposal; import org.springblade.transport.pojo.vo.ExceptionDisposalVO; import org.springblade.transport.service.IExceptionDisposalService; import org.springframework.web.bind.annotation.GetMapping; @@ -44,13 +45,14 @@ import org.springframework.web.bind.annotation.RestController; /** * 异常处置控制器 - * - * @author Chill + *

+ * 对外路径:{@code /api/blade-transport/exception-disposal/**} + * 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。 + * 司机上报(submit)/ 列表 / 详情仅需登录态;跟进与完成保留菜单鉴权。 */ @RestController @AllArgsConstructor -@PreAuth(menu = "exception_disposal") -@RequestMapping("/exception-disposal") +@RequestMapping({"/exception-disposal", "/blade-transport/exception-disposal"}) @Tag(name = "异常处置", description = "异常处置") public class ExceptionDisposalController extends BladeController { @@ -70,8 +72,16 @@ public class ExceptionDisposalController extends BladeController { return R.data(exceptionDisposalService.detail(id)); } - @PostMapping("/follow") + @PostMapping("/submit") @ApiOperationSupport(order = 3) + @Operation(summary = "异常上报", description = "司机端上报异常;上报人取登录态,运单信息按 waybillId/waybillNo 回填") + public R submit(@RequestBody ExceptionDisposal request) { + return R.data(exceptionDisposalService.submitReport(request)); + } + + @PostMapping("/follow") + @PreAuth(menu = "exception_disposal") + @ApiOperationSupport(order = 4) @Operation(summary = "异常跟进") public R follow(@RequestBody ExceptionDisposalFollowRequest request) { exceptionDisposalService.follow(request); @@ -79,7 +89,8 @@ public class ExceptionDisposalController extends BladeController { } @PostMapping("/complete") - @ApiOperationSupport(order = 4) + @PreAuth(menu = "exception_disposal") + @ApiOperationSupport(order = 5) @Operation(summary = "完成异常") public R complete(@RequestBody ExceptionDisposalFollowRequest request) { exceptionDisposalService.complete(request.getId()); @@ -87,7 +98,8 @@ public class ExceptionDisposalController extends BladeController { } @PostMapping("/batch-complete") - @ApiOperationSupport(order = 5) + @PreAuth(menu = "exception_disposal") + @ApiOperationSupport(order = 6) @Operation(summary = "批量完成异常") public R batchComplete(@RequestParam String ids) { exceptionDisposalService.batchComplete(ids); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index 36c761b..8479ad4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -53,6 +53,7 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillImportBatchVO; +import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.pojo.dto.WaybillMileageRequest; import org.springblade.transport.service.IContractManageService; @@ -100,6 +101,14 @@ public class WaybillController extends BladeController { return R.data(waybillService.detail(id)); } + @GetMapping("/punch-records") + @ApiOperationSupport(order = 1) + @Operation(summary = "打卡记录与司机上传", description = "返回节点/在途打卡流水,以及司机上传凭证图(label=节点-凭证类型)") + public R punchRecords( + @Parameter(description = "运单ID", required = true) @RequestParam Long waybillId) { + return R.data(waybillService.listPunchRecords(waybillId)); + } + @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页", description = "传入waybill") @@ -273,9 +282,9 @@ public class WaybillController extends BladeController { @PostMapping("/reassign") @ApiOperationSupport(order = 20) - @Operation(summary = "重新派单", description = "传入id") - public R reassign(@Parameter(description = "主键", required = true) @RequestParam Long id) { - return R.status(waybillService.reassign(id)); + @Operation(summary = "重新派单", description = "传入运单ID及新的司机、手机号、车牌") + public R reassign(@RequestBody Waybill waybill) { + return R.status(waybillService.reassign(waybill)); } @PostMapping("/complete") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillEnroutePunchMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillEnroutePunchMapper.java new file mode 100644 index 0000000..8cba06a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillEnroutePunchMapper.java @@ -0,0 +1,16 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.WaybillEnroutePunch; + +/** + * 运单在途打卡 Mapper + */ +@Mapper +public interface WaybillEnroutePunchMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillNodePunchMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillNodePunchMapper.java new file mode 100644 index 0000000..c9f2bcb --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/WaybillNodePunchMapper.java @@ -0,0 +1,16 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.WaybillNodePunch; + +/** + * 运单过程节点打卡 Mapper + */ +@Mapper +public interface WaybillNodePunchMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java new file mode 100644 index 0000000..5f357a4 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverAppService.java @@ -0,0 +1,47 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import org.springblade.transport.pojo.vo.DriverVehicleCardVO; +import org.springblade.transport.pojo.vo.DriverVO; + +import java.util.List; + +/** + * 司机端档案服务(小程序) + */ +public interface IDriverAppService { + + /** + * 当前登录用户对应的司机档案(按手机号匹配 blade_transport_driver.mobile) + * + * @param mobile 小程序可显式传入当前用户手机号;为空时从登录态解析 + */ + DriverVO currentByPhone(String mobile); + + /** + * 当前司机绑定的车辆列表(driver.driving_vehicle ↔ vehicle.plate_no) + */ + List myVehicles(); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java new file mode 100644 index 0000000..f9e804d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java @@ -0,0 +1,96 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.transport.pojo.dto.EnrouteSubmitDTO; +import org.springblade.transport.pojo.dto.NodeSubmitDTO; +import org.springblade.transport.pojo.vo.DriverEnrouteRecordVO; +import org.springblade.transport.pojo.vo.DriverNodePunchVO; +import org.springblade.transport.pojo.vo.DriverWaybillCardVO; +import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO; +import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO; + +/** + * 司机端运单服务(小程序首页 / 列表) + */ +public interface IDriverWaybillService { + + /** + * 当前登录司机的运输中任务(最多一条) + */ + DriverWaybillCardVO currentTask(); + + /** + * 当前登录司机的待接运单预览 + * + * @param size 预览条数,默认 2 + */ + DriverWaybillPreviewVO pendingPreview(Integer size); + + /** + * 列表 Tab 统计:全部 / 待接单 / 进行中 / 已完成 + */ + DriverWaybillTabCountsVO tabCounts(); + + /** + * 司机运单分页 + * + * @param current 当前页,从 1 开始 + * @param size 每页条数 + * @param status 小程序状态:空=全部,0待接单,1运输中,2已完成 + * @param keyword 关键字(运单号 / 起终点,可选) + */ + IPage page(Integer current, Integer size, Integer status, String keyword); + + /** + * 司机运单详情(含是否需要确认接单 requireAccept、在途打卡可见性等) + */ + DriverWaybillCardVO detail(Long id); + + /** + * 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。 + */ + boolean accept(Long id); + + /** + * 司机拒绝接单:过程配置要求接单且尚未接单时,写入拒单记录,运单保持待执行。 + */ + boolean reject(Long id, String reason); + + /** + * 提交在途打卡(过程配置在途节点 punch=是,且满足频次/时段)。 + */ + DriverEnrouteRecordVO submitEnroute(EnrouteSubmitDTO dto); + + /** + * 提交过程节点打卡(到场/装货/卸货/签收等 punch=是;在途请走 submitEnroute)。 + */ + DriverNodePunchVO submitNode(NodeSubmitDTO dto); + + /** + * 司机完成运单:校验归属后改状态为已完成,并走与管理端相同的应收应付明细生成逻辑。 + */ + boolean complete(Long id); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java index 8c51818..b04ee26 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java @@ -39,6 +39,11 @@ public interface IExceptionDisposalService extends BaseService { IPage selectWaybillPage(IPage page, WaybillVO waybill); WaybillVO detail(Long id); + + /** + * 管理端:运单打卡记录 + 司机上传凭证图(label=节点-凭证类型) + */ + WaybillPunchRecordsVO listPunchRecords(Long waybillId); + + Waybill syncDriverAcceptState(Waybill waybill); boolean submit(Waybill waybill); boolean saveDraft(Waybill waybill); BusinessRemoveResultVO removeWaybill(String ids); @@ -51,8 +59,15 @@ public interface IWaybillService extends BaseService { boolean changeRoute(Waybill waybill); boolean maintainMileage(WaybillMileageRequest request); boolean cancel(Long id); - boolean reassign(Long id); + boolean reassign(Waybill waybill); boolean complete(Long id); + + /** + * 司机端完成运单:跳过管理端部门校验,其余逻辑与 {@link #complete(Long)} 一致 + * (改状态 + 生成应收应付明细 + 尝试完成配载单)。 + */ + boolean completeWithoutDeptCheck(Long id); + BusinessRemoveResultVO batchComplete(String ids); LoadingManageVO roadLoading(String ids); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java new file mode 100644 index 0000000..551479d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverAppServiceImpl.java @@ -0,0 +1,210 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.feign.IUserClient; +import org.springblade.system.pojo.entity.User; +import org.springblade.transport.pojo.entity.Driver; +import org.springblade.transport.pojo.entity.TransportVehicle; +import org.springblade.transport.pojo.vo.DriverVehicleCardVO; +import org.springblade.transport.pojo.vo.DriverVO; +import org.springblade.transport.service.IDriverAppService; +import org.springblade.transport.service.IDriverService; +import org.springblade.transport.service.ITransportVehicleService; +import org.springblade.transport.wrapper.DriverWrapper; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 司机端档案服务实现 + */ +@Service +@RequiredArgsConstructor +public class DriverAppServiceImpl implements IDriverAppService { + + private final IDriverService driverService; + private final ITransportVehicleService transportVehicleService; + private final IUserClient userClient; + + @Override + public DriverVO currentByPhone(String mobile) { + Driver driver = currentDriver(mobile); + return driver == null ? null : DriverWrapper.build().entityVO(driver); + } + + @Override + public List myVehicles() { + Driver driver = currentDriver(null); + if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) { + return List.of(); + } + List plates = splitPlates(driver.getDrivingVehicle()); + if (plates.isEmpty()) { + return List.of(); + } + // 精确匹配 + 规范化匹配(兼容库中带间隔符/横线的车牌) + List vehicles = transportVehicleService.list(Wrappers.lambdaQuery() + .and(w -> { + w.in(TransportVehicle::getPlateNo, plates); + for (String plate : plates) { + w.or().apply( + "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(plate_no),'·',''),'•',''),'.',''),'-',''),' ','') = {0}", + plate + ); + } + }) + .orderByDesc(TransportVehicle::getUpdateTime)); + // 去重(精确与规范化可能命中同一条) + Map uniq = new LinkedHashMap<>(); + for (TransportVehicle vehicle : vehicles) { + if (vehicle.getId() != null) { + uniq.putIfAbsent(vehicle.getId(), vehicle); + } + } + return uniq.values().stream().map(this::toCard).collect(Collectors.toList()); + } + + private Driver currentDriver(String mobileHint) { + Long userId = AuthUtil.getUserId(); + if (userId == null || userId <= 0) { + throw new ServiceException("未登录"); + } + String phone = resolvePhone(userId, mobileHint); + Driver driver = null; + if (Func.isNotEmpty(phone)) { + driver = driverService.getOne(Wrappers.lambdaQuery() + .eq(Driver::getMobile, phone) + .orderByDesc(Driver::getUpdateTime) + .last("LIMIT 1")); + } + if (driver == null) { + driver = driverService.getOne(Wrappers.lambdaQuery() + .eq(Driver::getUserId, userId) + .last("LIMIT 1")); + } + return driver; + } + + private List splitPlates(String drivingVehicle) { + String normalized = drivingVehicle.replace(",", ",").replace("、", ",").replace(";", ",") + .replace(";", ",").replace("/", ",").replace("|", ","); + Set plates = new LinkedHashSet<>(); + for (String part : Func.toStrList(",", normalized)) { + if (Func.isEmpty(part)) { + continue; + } + String plate = normalizePlate(part); + if (Func.isNotEmpty(plate)) { + plates.add(plate); + } + } + return new ArrayList<>(plates); + } + + /** 车牌规范化:去空格/横线/间隔符并转大写,便于与车辆表关联 */ + private String normalizePlate(String plateNo) { + if (Func.isEmpty(plateNo)) { + return ""; + } + return plateNo.trim().replaceAll("[\\s\\-·•..]", "").toUpperCase(Locale.ROOT); + } + + private DriverVehicleCardVO toCard(TransportVehicle vehicle) { + DriverVehicleCardVO card = new DriverVehicleCardVO(); + card.setId(vehicle.getId()); + card.setPlateNo(vehicle.getPlateNo()); + card.setVehicleType(vehicle.getVehicleType()); + card.setLicenseFrontUrl(Func.toStr(vehicle.getDrivingLicenseImage(), "")); + card.setLicenseBackUrl(firstNotEmpty(vehicle.getDrivingLicenseViceFront(), vehicle.getDrivingLicenseMainBack())); + card.setRoadTransportNo(Func.toStr(vehicle.getRoadTransportCertNo(), "")); + card.setRoadTransportUrl(Func.toStr(vehicle.getRoadTransportCertImage(), "")); + card.setVin(""); + card.setEngineNo(""); + if (vehicle.getDrivingLicenseEndDate() != null) { + card.setLicenseValidEnd(vehicle.getDrivingLicenseEndDate().toString()); + } else if (Integer.valueOf(1).equals(vehicle.getDrivingLicenseLongTerm())) { + card.setLicenseValidEnd("长期"); + } else { + card.setLicenseValidEnd(""); + } + card.setCertificationStatus(vehicle.getCertificationStatus()); + return card; + } + + private String firstNotEmpty(String first, String second) { + if (Func.isNotEmpty(first)) { + return first; + } + return Func.toStr(second, ""); + } + + /** + * 解析用于匹配司机档案的手机号。 + * 优先级:前端传入且与本人一致的 mobile → JWT account(司机账号多为手机号)→ 用户中心 phone/account + */ + private String resolvePhone(Long userId, String mobileHint) { + String selfPhone = resolveSelfPhone(userId); + String hint = Func.isEmpty(mobileHint) ? null : mobileHint.trim(); + if (Func.isNotEmpty(hint)) { + if (Func.isNotEmpty(selfPhone) && !selfPhone.equals(hint)) { + throw new ServiceException("只能查询本人司机档案"); + } + return hint; + } + return selfPhone; + } + + private String resolveSelfPhone(Long userId) { + String account = AuthUtil.getUserAccount(); + if (Func.isNotEmpty(account) && account.matches("^1\\d{10}$")) { + return account.trim(); + } + R result = userClient.userInfoById(userId); + if (result == null || !R.isSuccess(result) || result.getData() == null) { + return Func.isEmpty(account) ? null : account.trim(); + } + User user = result.getData(); + if (Func.isNotEmpty(user.getPhone())) { + return user.getPhone().trim(); + } + if (Func.isNotEmpty(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) { + return user.getAccount().trim(); + } + return Func.isEmpty(account) ? null : account.trim(); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java new file mode 100644 index 0000000..2071910 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -0,0 +1,1069 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.WaybillEnroutePunchMapper; +import org.springblade.transport.mapper.WaybillNodePunchMapper; +import org.springblade.transport.pojo.dto.EnrouteSubmitDTO; +import org.springblade.transport.pojo.dto.NodeSubmitDTO; +import org.springblade.transport.pojo.entity.Driver; +import org.springblade.transport.pojo.entity.ProcessConfig; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.entity.WaybillEnroutePunch; +import org.springblade.transport.pojo.entity.WaybillNodePunch; +import org.springblade.transport.pojo.vo.DriverEnrouteRecordVO; +import org.springblade.transport.pojo.vo.DriverNodePunchVO; +import org.springblade.transport.pojo.vo.DriverPunchNodeVO; +import org.springblade.transport.pojo.vo.DriverPunchPhotoVO; +import org.springblade.transport.pojo.vo.DriverWaybillCardVO; +import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO; +import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO; +import org.springblade.transport.service.IDriverService; +import org.springblade.transport.service.IDriverWaybillService; +import org.springblade.transport.service.IProcessConfigService; +import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.WaybillProcessSupport; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 司机端运单服务实现 + */ +@Service +@RequiredArgsConstructor +public class DriverWaybillServiceImpl implements IDriverWaybillService { + + private static final String STATUS_PENDING = "pending"; + private static final String STATUS_WAITING_DISPATCH = "waiting_dispatch"; + private static final String STATUS_DISPATCHING = "dispatching"; + private static final String STATUS_RUNNING = "running"; + private static final String STATUS_COMPLETED = "completed"; + private static final String STATUS_CANCELLED = "cancelled"; + + /** 小程序「待接单」对应的后端业务状态 */ + private static final List PENDING_STATUSES = Arrays.asList( + STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING + ); + /** Tab「全部」统计口径:待接 + 运输中 + 已完成(不含已取消) */ + private static final List TAB_ALL_STATUSES = Arrays.asList( + STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING, STATUS_COMPLETED + ); + + private static final int DEFAULT_PREVIEW_SIZE = 2; + private static final int MAX_PREVIEW_SIZE = 20; + private static final int DEFAULT_PAGE_SIZE = 10; + private static final int MAX_PAGE_SIZE = 50; + private static final DateTimeFormatter DATE_DOT = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + private static final DateTimeFormatter DATE_DOT_SHORT = DateTimeFormatter.ofPattern("MM.dd"); + private static final DateTimeFormatter TIME_HM = DateTimeFormatter.ofPattern("HH:mm"); + + private final IWaybillService waybillService; + private final IDriverService driverService; + private final IProcessConfigService processConfigService; + private final WaybillEnroutePunchMapper enroutePunchMapper; + private final WaybillNodePunchMapper nodePunchMapper; + + @Override + public DriverWaybillCardVO currentTask() { + List plates = currentBoundPlates(); + if (plates.isEmpty()) { + return null; + } + // 进行中:running,或无需确认接单但仍为 pending 的历史数据 + List candidates = waybillService.list(scopedQuery(plates) + .in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING)) + .orderByDesc(Waybill::getUpdateTime) + .last("LIMIT 20")); + for (Waybill waybill : candidates) { + Waybill normalized = normalizeAcceptStatus(waybill); + if (STATUS_RUNNING.equals(normalized.getBusinessStatus())) { + return toCard(normalized); + } + } + return null; + } + + @Override + public DriverWaybillPreviewVO pendingPreview(Integer size) { + DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO(); + List plates = currentBoundPlates(); + if (plates.isEmpty()) { + return preview; + } + int limit = normalizePreviewSize(size); + List pendingList = waybillService.list(scopedQuery(plates) + .in(Waybill::getBusinessStatus, PENDING_STATUSES) + .orderByDesc(Waybill::getCreateTime)); + List needAccept = pendingList.stream() + .map(this::normalizeAcceptStatus) + .filter(w -> STATUS_PENDING.equals(w.getBusinessStatus())) + .collect(Collectors.toList()); + preview.setTotal((long) needAccept.size()); + preview.setRecords(needAccept.stream().limit(limit).map(this::toCard).collect(Collectors.toList())); + return preview; + } + + @Override + public DriverWaybillTabCountsVO tabCounts() { + DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO(); + List plates = currentBoundPlates(); + if (plates.isEmpty()) { + return vo; + } + List waybills = waybillService.list(scopedQuery(plates) + .in(Waybill::getBusinessStatus, TAB_ALL_STATUSES)); + long pending = 0; + long doing = 0; + long done = 0; + for (Waybill waybill : waybills) { + Waybill normalized = normalizeAcceptStatus(waybill); + Integer appStatus = toAppStatus(normalized.getBusinessStatus()); + if (appStatus == null) { + continue; + } + if (appStatus == 0) { + pending++; + } else if (appStatus == 1) { + doing++; + } else if (appStatus == 2) { + done++; + } + } + vo.setPending(pending); + vo.setDoing(doing); + vo.setDone(done); + vo.setAll(pending + doing + done); + return vo; + } + + @Override + public IPage page(Integer current, Integer size, Integer status, String keyword) { + int pageNo = current == null || current < 1 ? 1 : current; + int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE); + Page empty = new Page<>(pageNo, pageSize); + + List plates = currentBoundPlates(); + if (plates.isEmpty()) { + return empty; + } + + LambdaQueryWrapper wrapper = scopedQuery(plates); + // 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页 + applyStatusFilterForQuery(wrapper, status); + applyKeyword(wrapper, keyword); + wrapper.orderByDesc(Waybill::getCreateTime); + + List candidates = waybillService.list(wrapper); + List cards = candidates.stream() + .map(this::normalizeAcceptStatus) + .filter(w -> matchAppStatus(w, status)) + .map(this::toCard) + .collect(Collectors.toList()); + + long total = cards.size(); + int from = Math.min((pageNo - 1) * pageSize, cards.size()); + int to = Math.min(from + pageSize, cards.size()); + Page page = new Page<>(pageNo, pageSize, total); + page.setRecords(cards.subList(from, to)); + return page; + } + + @Override + public DriverWaybillCardVO detail(Long id) { + Driver driver = requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + return toCard(normalizeAcceptStatus(waybill), true); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DriverEnrouteRecordVO submitEnroute(EnrouteSubmitDTO dto) { + if (dto == null || dto.getWaybillId() == null) { + throw new ServiceException("运单ID不能为空"); + } + Driver driver = requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver)); + waybill = normalizeAcceptStatus(waybill); + + Date lastPunchAt = findLastPunchTime(waybill.getId()); + WaybillProcessSupport.TransitCheckinDecision decision = WaybillProcessSupport.evaluateTransitCheckin( + resolveProcessJson(waybill), waybill.getBusinessStatus(), lastPunchAt, LocalDateTime.now()); + if (!decision.punchEnabled()) { + throw new ServiceException("该运单未启用在途打卡"); + } + if (decision.doneToday()) { + throw new ServiceException("今日已完成在途打卡,不可重复打卡"); + } + // 不做频次/时段门禁;定位按前端是否传参落库 + + Date now = new Date(); + WaybillEnroutePunch punch = new WaybillEnroutePunch(); + punch.setWaybillId(waybill.getId()); + punch.setWaybillNo(waybill.getWaybillNo()); + punch.setDriverId(driver.getId()); + punch.setPunchTime(now); + if (dto.getLocation() != null) { + if (dto.getLocation().getLongitude() != null) { + punch.setLongitude(BigDecimal.valueOf(dto.getLocation().getLongitude())); + } + if (dto.getLocation().getLatitude() != null) { + punch.setLatitude(BigDecimal.valueOf(dto.getLocation().getLatitude())); + } + punch.setAddress(Func.toStr(dto.getLocation().getAddress(), "").trim()); + } + punch.setPhoto(Func.isEmpty(dto.getPhoto()) ? null : dto.getPhoto().trim()); + enroutePunchMapper.insert(punch); + return toEnrouteRecord(punch); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DriverNodePunchVO submitNode(NodeSubmitDTO dto) { + if (dto == null || dto.getWaybillId() == null) { + throw new ServiceException("运单ID不能为空"); + } + String nodeCode = Func.toStr(dto.getNodeCode(), "").trim(); + if (Func.isEmpty(nodeCode)) { + throw new ServiceException("节点编码不能为空"); + } + Driver driver = requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver)); + waybill = normalizeAcceptStatus(waybill); + + Map nodeCfg = findPunchNodeConfig(resolveProcessJson(waybill), nodeCode); + if (nodeCfg == null) { + throw new ServiceException("该节点未启用打卡或不存在"); + } + if (WaybillProcessSupport.isTransitNodePublic(nodeCfg)) { + throw new ServiceException("在途打卡请使用在途打卡接口"); + } + WaybillNodePunch existed = findNodePunch(latestNodePunchMap(waybill.getId()), nodeCfg); + if (existed != null) { + throw new ServiceException("该节点已打卡,不可重复打卡"); + } + + Date now = new Date(); + WaybillNodePunch punch = new WaybillNodePunch(); + punch.setWaybillId(waybill.getId()); + punch.setWaybillNo(waybill.getWaybillNo()); + punch.setDriverId(driver.getId()); + punch.setNodeCode(WaybillProcessSupport.nodeKey(nodeCfg)); + punch.setNodeName(WaybillProcessSupport.nodeName(nodeCfg)); + punch.setPunchTime(now); + if (dto.getLocation() != null) { + if (dto.getLocation().getLongitude() != null) { + punch.setLongitude(BigDecimal.valueOf(dto.getLocation().getLongitude())); + } + if (dto.getLocation().getLatitude() != null) { + punch.setLatitude(BigDecimal.valueOf(dto.getLocation().getLatitude())); + } + punch.setAddress(Func.toStr(dto.getLocation().getAddress(), "").trim()); + } + if (dto.getPhotos() != null && !dto.getPhotos().isEmpty()) { + List urls = dto.getPhotos().stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(Func::isNotEmpty) + .collect(Collectors.toList()); + if (!urls.isEmpty()) { + List types = WaybillProcessSupport.nodeStringList(nodeCfg, "voucherTypes"); + List> photoItems = new ArrayList<>(); + for (int i = 0; i < urls.size(); i++) { + Map item = new LinkedHashMap<>(); + String type = i < types.size() ? types.get(i) : ("凭证" + (i + 1)); + item.put("type", type); + item.put("url", urls.get(i)); + photoItems.add(item); + } + punch.setPhotos(JsonUtil.toJson(photoItems)); + } + } + punch.setWeight(trimOrNull(dto.getWeight())); + punch.setVolume(trimOrNull(dto.getVolume())); + punch.setQuantity(trimOrNull(dto.getQuantity())); + punch.setRemark(trimOrNull(dto.getRemark())); + punch.setExceptionFlag(Boolean.TRUE.equals(dto.getException()) ? 1 : 0); + nodePunchMapper.insert(punch); + + advanceCurrentProcessNode(waybill, nodeCfg); + return toNodePunchVO(punch); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean accept(Long id) { + Driver driver = requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + waybillService.syncDriverAcceptState(waybill); + assertAcceptable(waybill); + Date now = new Date(); + waybill.setDriverAcceptStatus(WaybillProcessSupport.ACCEPT_ACCEPTED); + waybill.setDriverAcceptTime(now); + waybill.setDriverAcceptDriverId(driver.getId()); + waybill.setDriverRejectTime(null); + waybill.setDriverRejectReason(null); + waybill.setBusinessStatus(STATUS_RUNNING); + return waybillService.updateById(waybill); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean reject(Long id, String reason) { + Driver driver = requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + waybillService.syncDriverAcceptState(waybill); + assertRejectable(waybill); + String rejectReason = Func.isEmpty(reason) ? null : reason.trim(); + if (Func.isNotEmpty(rejectReason) && rejectReason.length() > 200) { + throw new ServiceException("拒绝原因不能超过200字"); + } + Date now = new Date(); + waybill.setDriverAcceptStatus(WaybillProcessSupport.ACCEPT_REJECTED); + waybill.setDriverAcceptTime(null); + waybill.setDriverAcceptDriverId(null); + waybill.setDriverRejectTime(now); + waybill.setDriverRejectReason(rejectReason); + waybill.setBusinessStatus(STATUS_PENDING); + return waybillService.updateById(waybill); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean complete(Long id) { + if (id == null) { + throw new ServiceException("运单ID不能为空"); + } + Driver driver = requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + waybill = normalizeAcceptStatus(waybill); + if (!STATUS_RUNNING.equals(waybill.getBusinessStatus()) + && !STATUS_PENDING.equals(waybill.getBusinessStatus()) + && !STATUS_WAITING_DISPATCH.equals(waybill.getBusinessStatus()) + && !STATUS_DISPATCHING.equals(waybill.getBusinessStatus())) { + throw new ServiceException("当前运单状态不允许完成"); + } + // 与管理端完成逻辑一致:改状态 + 生成应收应付明细 + 尝试完成配载单 + return waybillService.completeWithoutDeptCheck(waybill.getId()); + } + + private void assertAcceptable(Waybill waybill) { + if (WaybillProcessSupport.isTerminalBusinessStatus(waybill.getBusinessStatus())) { + throw new ServiceException("当前运单状态不允许接单"); + } + if (!WaybillProcessSupport.requiresDriverAcceptConfirmation(resolveProcessJson(waybill))) { + throw new ServiceException("该运单无需确认接单"); + } + if (WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus())) { + throw new ServiceException("该运单已接单"); + } + } + + private void assertRejectable(Waybill waybill) { + if (WaybillProcessSupport.isTerminalBusinessStatus(waybill.getBusinessStatus())) { + throw new ServiceException("当前运单状态不允许拒绝接单"); + } + if (!WaybillProcessSupport.requiresDriverAcceptConfirmation(resolveProcessJson(waybill))) { + throw new ServiceException("该运单无需确认接单"); + } + if (WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus())) { + throw new ServiceException("该运单已接单,无法拒绝"); + } + } + + private Waybill loadDriverWaybill(Long id, List plates) { + if (id == null) { + throw new ServiceException("运单ID不能为空"); + } + if (plates.isEmpty()) { + throw new ServiceException("当前司机未绑定车辆"); + } + Waybill waybill = waybillService.getOne(scopedQuery(plates).eq(Waybill::getId, id).last("LIMIT 1")); + if (waybill == null) { + throw new ServiceException("运单不存在或无权操作"); + } + return waybill; + } + + private Driver requireCurrentDriver() { + Driver driver = currentDriver(); + if (driver == null) { + throw new ServiceException("未找到当前登录司机档案"); + } + return driver; + } + + private List currentBoundPlates(Driver driver) { + if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) { + return List.of(); + } + return splitPlates(driver.getDrivingVehicle()); + } + + /** + * 查询侧状态条件:进行中需包含可能被校正的 pending;待接单只查 pending 类。 + */ + private void applyStatusFilterForQuery(LambdaQueryWrapper wrapper, Integer status) { + if (status == null) { + wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES); + return; + } + switch (status) { + case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES); + case 1 -> wrapper.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING, + STATUS_WAITING_DISPATCH, STATUS_DISPATCHING)); + case 2 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_COMPLETED); + case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED); + default -> wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES); + } + } + + private boolean matchAppStatus(Waybill waybill, Integer status) { + if (status == null) { + Integer app = toAppStatus(waybill.getBusinessStatus()); + return app != null && app >= 0 && app <= 2; + } + return Objects.equals(toAppStatus(waybill.getBusinessStatus()), status); + } + + /** + * 无过程配置或无需确认接单的 pending 运单,落库校正为 running; + * 需要确认接单且尚未接单的 running 运单,落库校正为 pending。 + */ + private Waybill normalizeAcceptStatus(Waybill waybill) { + return waybillService.syncDriverAcceptState(waybill); + } + + /** + * 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内。 + * 绑定来源:blade_transport_driver.driving_vehicle + */ + private LambdaQueryWrapper scopedQuery(List plates) { + return Wrappers.lambdaQuery().and(w -> { + w.in(Waybill::getVehicleNo, plates) + .or().in(Waybill::getTrailerVehicleNo, plates); + for (String plate : plates) { + w.or().apply( + "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}", + plate + ); + w.or().apply( + "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(trailer_vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}", + plate + ); + } + }); + } + + private void applyKeyword(LambdaQueryWrapper wrapper, String keyword) { + if (Func.isEmpty(keyword)) { + return; + } + String kw = keyword.trim(); + wrapper.and(w -> w.like(Waybill::getWaybillNo, kw) + .or().like(Waybill::getDepartureName, kw) + .or().like(Waybill::getArrivalName, kw) + .or().like(Waybill::getDepartureAddress, kw) + .or().like(Waybill::getArrivalAddress, kw) + .or().like(Waybill::getVehicleNo, kw)); + } + + /** 当前登录司机绑定的规范化车牌列表;无司机或无绑定车牌则空 */ + private List currentBoundPlates() { + Driver driver = currentDriver(); + if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) { + return List.of(); + } + return splitPlates(driver.getDrivingVehicle()); + } + + /** + * 当前登录司机:优先 userId,其次 JWT 账号(手机号)匹配 driver.mobile + */ + private Driver currentDriver() { + Long userId = AuthUtil.getUserId(); + if (userId == null || userId <= 0) { + return null; + } + Driver driver = driverService.getOne(Wrappers.lambdaQuery() + .eq(Driver::getUserId, userId) + .last("LIMIT 1")); + if (driver != null) { + return driver; + } + String account = AuthUtil.getUserAccount(); + if (Func.isNotEmpty(account) && account.matches("^1\\d{10}$")) { + return driverService.getOne(Wrappers.lambdaQuery() + .eq(Driver::getMobile, account.trim()) + .orderByDesc(Driver::getUpdateTime) + .last("LIMIT 1")); + } + return null; + } + + private List splitPlates(String drivingVehicle) { + String normalized = drivingVehicle.replace(",", ",").replace("、", ",").replace(";", ",") + .replace(";", ",").replace("/", ",").replace("|", ","); + Set plates = new LinkedHashSet<>(); + for (String part : Func.toStrList(",", normalized)) { + if (Func.isEmpty(part)) { + continue; + } + String plate = normalizePlate(part); + if (Func.isNotEmpty(plate)) { + plates.add(plate); + } + } + return new ArrayList<>(plates); + } + + private String normalizePlate(String plateNo) { + if (Func.isEmpty(plateNo)) { + return ""; + } + return plateNo.trim().replaceAll("[\\s\\-·•..]", "").toUpperCase(Locale.ROOT); + } + + private int normalizePreviewSize(Integer size) { + if (size == null || size < 1) { + return DEFAULT_PREVIEW_SIZE; + } + return Math.min(size, MAX_PREVIEW_SIZE); + } + + private DriverWaybillCardVO toCard(Waybill waybill) { + return toCard(waybill, false); + } + + private DriverWaybillCardVO toCard(Waybill waybill, boolean withEnrouteRecords) { + DriverWaybillCardVO card = new DriverWaybillCardVO(); + card.setId(waybill.getId()); + card.setWaybillNo(waybill.getWaybillNo()); + card.setFromName(Func.toStr(waybill.getDepartureName(), "")); + card.setToName(Func.toStr(waybill.getArrivalName(), "")); + card.setFromAddress(Func.toStr(waybill.getDepartureAddress(), card.getFromName())); + card.setToAddress(Func.toStr(waybill.getArrivalAddress(), card.getToName())); + card.setCargoNames(splitCargoNames(waybill.getCargoName())); + card.setCargoCategory(Func.toStr(waybill.getCargoType(), "")); + card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())); + card.setStatus(toAppStatus(waybill.getBusinessStatus())); + String createTime = formatDateTime(waybill.getCreateTime()); + card.setCreateTime(createTime); + card.setPublishTime(createTime); + card.setCurrentNode(Func.toStr(waybill.getCurrentProcessNode(), "")); + card.setTimeRange(formatTimeRange(waybill)); + card.setFreight(resolveFreight(waybill)); + card.setDriverName(Func.toStr(waybill.getDriverName(), "")); + card.setDriverPhone(Func.toStr(waybill.getDriverPhone(), "")); + card.setVehicleNo(Func.toStr(waybill.getVehicleNo(), "")); + String processJson = resolveProcessJson(waybill); + card.setRequireAccept(WaybillProcessSupport.requiresDriverAcceptConfirmation(processJson)); + card.setAcceptStatus(waybill.getDriverAcceptStatus()); + card.setRejectReason(waybill.getDriverRejectReason()); + + if (withEnrouteRecords) { + Date lastPunchAt = findLastPunchTime(waybill.getId()); + WaybillProcessSupport.TransitCheckinDecision transit = WaybillProcessSupport.evaluateTransitCheckin( + processJson, waybill.getBusinessStatus(), lastPunchAt, LocalDateTime.now()); + card.setTransitPunchEnabled(transit.punchEnabled()); + card.setTransitCheckinVisible(transit.visible()); + card.setRequireTransitCheckinToday(transit.dueToday()); + card.setTransitCheckinDoneToday(transit.doneToday()); + card.setTransitFrequencyDays(transit.frequencyDays()); + card.setTransitTimeStart(transit.timeStart()); + card.setTransitTimeEnd(transit.timeEnd()); + card.setEnrouteRecords(listEnrouteRecords(waybill.getId())); + card.setPunchNodes(buildPunchNodes(waybill, transit, processJson)); + } else { + // 列表/首页:只解析过程配置是否启用在途打卡,不做频次/时段与落库查询 + boolean punchEnabled = WaybillProcessSupport.isTransitPunchEnabled(processJson); + card.setTransitPunchEnabled(punchEnabled); + card.setTransitCheckinVisible(null); + card.setRequireTransitCheckinToday(null); + card.setTransitCheckinDoneToday(null); + } + return card; + } + + /** + * 动态获取项目启用中的过程配置节点 JSON;无则回退运单快照 processJson。 + */ + private String resolveProcessJson(Waybill waybill) { + if (waybill == null) { + return null; + } + String live = loadLiveProcessConfigJson(waybill.getProjectId()); + if (Func.isNotEmpty(live)) { + return live; + } + return waybill.getProcessJson(); + } + + private String loadLiveProcessConfigJson(Long projectId) { + if (projectId == null) { + return null; + } + String projectIdStr = String.valueOf(projectId); + return processConfigService.list(Wrappers.lambdaQuery() + .eq(ProcessConfig::getStatus, 1) + .eq(ProcessConfig::getIsDeleted, 0) + .like(ProcessConfig::getProjectIds, projectIdStr) + .orderByDesc(ProcessConfig::getUpdateTime) + .orderByDesc(ProcessConfig::getCreateTime)) + .stream() + .filter(cfg -> containsProjectId(cfg.getProjectIds(), projectIdStr)) + .map(ProcessConfig::getNodeConfigJson) + .filter(Func::isNotEmpty) + .findFirst() + .orElse(null); + } + + private boolean containsProjectId(String projectIds, String projectId) { + if (Func.isEmpty(projectIds) || Func.isEmpty(projectId)) { + return false; + } + return Arrays.stream(projectIds.split(",")) + .map(String::trim) + .anyMatch(projectId::equals); + } + + /** + * 组装过程配置 punch=是 的打卡节点列表。 + *

+ * 未打卡节点均可打,不做顺序/时段门禁; + * 默认展开:第一个未完成的可见节点。 + * 非在途节点「已打卡」以节点打卡表为准。 + * 节点字段(定位/货量/凭证)取自动态过程配置。 + */ + private List buildPunchNodes( + Waybill waybill, + WaybillProcessSupport.TransitCheckinDecision transit, + String processJson + ) { + List> punchConfigs = WaybillProcessSupport.listDriverPunchNodes(processJson); + if (punchConfigs.isEmpty()) { + return Collections.emptyList(); + } + + List nodes = new ArrayList<>(); + DriverEnrouteRecordVO latestEnroute = null; + List enroutes = listEnrouteRecords(waybill.getId()); + if (!enroutes.isEmpty()) { + latestEnroute = enroutes.get(enroutes.size() - 1); + } + Map latestNodePunchByCode = latestNodePunchMap(waybill.getId()); + + for (Map cfg : punchConfigs) { + boolean isTransit = WaybillProcessSupport.isTransitNodePublic(cfg); + DriverPunchNodeVO vo = new DriverPunchNodeVO(); + vo.setKey(WaybillProcessSupport.nodeKey(cfg)); + vo.setName(WaybillProcessSupport.nodeName(cfg)); + vo.setTransit(isTransit); + vo.setNeedLocation(WaybillProcessSupport.nodeNeedLocation(cfg)); + vo.setNeedCargo(WaybillProcessSupport.nodeNeedCargo(cfg)); + vo.setCargoTypes(WaybillProcessSupport.nodeStringList(cfg, "cargoTypes")); + vo.setNeedVoucher(WaybillProcessSupport.nodeNeedVoucher(cfg)); + vo.setVoucherTypes(WaybillProcessSupport.nodeStringList(cfg, "voucherTypes")); + vo.setDefaultExpanded(false); + + if (isTransit) { + // 在途:今日已打则不可再打,回显最新一次信息 + boolean punchOn = transit != null && transit.punchEnabled(); + boolean done = transit != null && transit.doneToday(); + if (!punchOn) { + continue; + } + vo.setVisible(true); + vo.setDone(done); + vo.setActionable(!done); + if (done && latestEnroute != null) { + vo.setCheckinTime(latestEnroute.getTime()); + vo.setCheckinPlace(latestEnroute.getAddress()); + if (Func.isNotEmpty(latestEnroute.getPhoto())) { + DriverPunchPhotoVO photo = new DriverPunchPhotoVO(); + String voucherType = vo.getVoucherTypes().isEmpty() ? "货物照片" : vo.getVoucherTypes().get(0); + photo.setType(voucherType); + photo.setLabel(vo.getName() + "-" + voucherType); + photo.setUrl(latestEnroute.getPhoto()); + vo.setPhotos(List.of(photo)); + } + } + nodes.add(vo); + continue; + } + + WaybillNodePunch punched = findNodePunch(latestNodePunchByCode, cfg); + boolean done = punched != null; + vo.setVisible(true); + vo.setDone(done); + vo.setActionable(!done); + if (done) { + if (punched.getPunchTime() != null) { + LocalDateTime ldt = punched.getPunchTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + vo.setCheckinTime(ldt.format(TIME_HM)); + } + vo.setCheckinPlace(Func.toStr(punched.getAddress(), "")); + vo.setWeight(punched.getWeight()); + vo.setVolume(punched.getVolume()); + vo.setQuantity(punched.getQuantity()); + vo.setPhotos(decodeDriverPunchPhotos( + punched.getPhotos(), + vo.getName(), + vo.getVoucherTypes())); + } + nodes.add(vo); + } + + for (DriverPunchNodeVO vo : nodes) { + if (!Boolean.TRUE.equals(vo.getDone())) { + vo.setDefaultExpanded(true); + break; + } + } + return nodes; + } + + private Map findPunchNodeConfig(String processJson, String nodeCode) { + List> punchConfigs = WaybillProcessSupport.listDriverPunchNodes(processJson); + for (Map cfg : punchConfigs) { + String key = WaybillProcessSupport.nodeKey(cfg); + String name = WaybillProcessSupport.nodeName(cfg); + if (nodeCode.equalsIgnoreCase(key) || nodeCode.equals(name)) { + return cfg; + } + } + return null; + } + + /** 打卡后推进运单当前过程节点到下一启用节点(若已是末节点则保持本节点) */ + private void advanceCurrentProcessNode(Waybill waybill, Map punchedCfg) { + List> enabled = WaybillProcessSupport.listEnabledProcessNodes(resolveProcessJson(waybill)); + int idx = indexInEnabled(enabled, punchedCfg); + if (idx < 0) { + waybill.setCurrentProcessNode(WaybillProcessSupport.nodeKey(punchedCfg)); + waybillService.updateById(waybill); + return; + } + if (idx + 1 < enabled.size()) { + waybill.setCurrentProcessNode(WaybillProcessSupport.nodeKey(enabled.get(idx + 1))); + } else { + waybill.setCurrentProcessNode(WaybillProcessSupport.nodeKey(punchedCfg)); + } + waybillService.updateById(waybill); + } + + private Map latestNodePunchMap(Long waybillId) { + Map map = new HashMap<>(); + if (waybillId == null) { + return map; + } + List list = nodePunchMapper.selectList(Wrappers.lambdaQuery() + .eq(WaybillNodePunch::getWaybillId, waybillId) + .orderByAsc(WaybillNodePunch::getPunchTime)); + for (WaybillNodePunch punch : list) { + String code = Func.toStr(punch.getNodeCode(), "").trim(); + if (Func.isNotEmpty(code)) { + map.put(code.toLowerCase(Locale.ROOT), punch); + } + } + return map; + } + + private WaybillNodePunch findNodePunch(Map map, Map cfg) { + if (map == null || map.isEmpty() || cfg == null) { + return null; + } + String key = WaybillProcessSupport.nodeKey(cfg); + if (Func.isNotEmpty(key)) { + WaybillNodePunch hit = map.get(key.toLowerCase(Locale.ROOT)); + if (hit != null) { + return hit; + } + } + String name = WaybillProcessSupport.nodeName(cfg); + if (Func.isNotEmpty(name)) { + return map.get(name.toLowerCase(Locale.ROOT)); + } + return null; + } + + @SuppressWarnings("unchecked") + private List decodeDriverPunchPhotos(String raw, String nodeName, List voucherTypes) { + List out = new ArrayList<>(); + if (Func.isEmpty(raw)) { + return out; + } + String text = raw.trim(); + List types = voucherTypes == null ? List.of() : voucherTypes; + if (text.startsWith("[")) { + try { + List list = JsonUtil.parse(text, List.class); + if (list != null) { + int i = 0; + for (Object item : list) { + if (item instanceof Map map) { + String url = Func.toStr(map.get("url"), "").trim(); + if (Func.isEmpty(url)) { + continue; + } + String type = Func.toStr(map.get("type"), "").trim(); + if (Func.isEmpty(type) && i < types.size()) { + type = types.get(i); + } + if (Func.isEmpty(type)) { + type = "凭证" + (i + 1); + } + out.add(buildDriverPunchPhoto(nodeName, type, url)); + i++; + } else if (item != null && Func.isNotEmpty(String.valueOf(item).trim())) { + String type = i < types.size() ? types.get(i) : ("凭证" + (i + 1)); + out.add(buildDriverPunchPhoto(nodeName, type, String.valueOf(item).trim())); + i++; + } + } + return out; + } + } catch (Exception ignored) { + // fall through + } + } + String[] urls = text.split(","); + for (int i = 0; i < urls.length; i++) { + String url = urls[i].trim(); + if (Func.isEmpty(url)) { + continue; + } + String type = i < types.size() ? types.get(i) : ("凭证" + (i + 1)); + out.add(buildDriverPunchPhoto(nodeName, type, url)); + } + return out; + } + + private DriverPunchPhotoVO buildDriverPunchPhoto(String nodeName, String type, String url) { + DriverPunchPhotoVO photo = new DriverPunchPhotoVO(); + photo.setType(type); + photo.setUrl(url); + photo.setLabel(Func.toStr(nodeName, "节点") + "-" + type); + return photo; + } + + private DriverNodePunchVO toNodePunchVO(WaybillNodePunch punch) { + DriverNodePunchVO vo = new DriverNodePunchVO(); + vo.setWaybillId(punch.getWaybillId()); + vo.setNodeCode(punch.getNodeCode()); + vo.setNodeName(punch.getNodeName()); + vo.setAddress(Func.toStr(punch.getAddress(), "")); + vo.setWeight(punch.getWeight()); + vo.setVolume(punch.getVolume()); + vo.setQuantity(punch.getQuantity()); + vo.setPhotos(extractPhotoUrls(punch.getPhotos())); + if (punch.getPunchTime() != null) { + vo.setCheckinTime(formatDateTime(punch.getPunchTime())); + } + return vo; + } + + @SuppressWarnings("unchecked") + private List extractPhotoUrls(String raw) { + List urls = new ArrayList<>(); + if (Func.isEmpty(raw)) { + return urls; + } + String text = raw.trim(); + if (text.startsWith("[")) { + try { + List list = JsonUtil.parse(text, List.class); + if (list != null) { + for (Object item : list) { + if (item instanceof Map map) { + String url = Func.toStr(map.get("url"), "").trim(); + if (Func.isNotEmpty(url)) { + urls.add(url); + } + } else if (item != null && Func.isNotEmpty(String.valueOf(item).trim())) { + urls.add(String.valueOf(item).trim()); + } + } + return urls; + } + } catch (Exception ignored) { + // fall through + } + } + return Arrays.stream(text.split(",")) + .map(String::trim) + .filter(Func::isNotEmpty) + .collect(Collectors.toList()); + } + + private static String trimOrNull(String value) { + String v = Func.toStr(value, "").trim(); + return Func.isEmpty(v) ? null : v; + } + + private int indexInEnabled(List> enabled, Map target) { + String key = WaybillProcessSupport.nodeKey(target); + String name = WaybillProcessSupport.nodeName(target); + for (int i = 0; i < enabled.size(); i++) { + Map n = enabled.get(i); + if (key.equals(WaybillProcessSupport.nodeKey(n)) || name.equals(WaybillProcessSupport.nodeName(n))) { + return i; + } + } + return -1; + } + + private Date findLastPunchTime(Long waybillId) { + if (waybillId == null) { + return null; + } + WaybillEnroutePunch latest = enroutePunchMapper.selectOne(Wrappers.lambdaQuery() + .eq(WaybillEnroutePunch::getWaybillId, waybillId) + .orderByDesc(WaybillEnroutePunch::getPunchTime) + .last("LIMIT 1")); + return latest == null ? null : latest.getPunchTime(); + } + + private List listEnrouteRecords(Long waybillId) { + if (waybillId == null) { + return Collections.emptyList(); + } + List punches = enroutePunchMapper.selectList(Wrappers.lambdaQuery() + .eq(WaybillEnroutePunch::getWaybillId, waybillId) + .orderByAsc(WaybillEnroutePunch::getPunchTime)); + return punches.stream().map(this::toEnrouteRecord).collect(Collectors.toList()); + } + + private DriverEnrouteRecordVO toEnrouteRecord(WaybillEnroutePunch punch) { + DriverEnrouteRecordVO vo = new DriverEnrouteRecordVO(); + vo.setAddress(Func.toStr(punch.getAddress(), "")); + vo.setPhoto(punch.getPhoto()); + if (punch.getPunchTime() != null) { + LocalDateTime ldt = punch.getPunchTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + vo.setTime(ldt.format(TIME_HM)); + } else { + vo.setTime(""); + } + return vo; + } + + /** + * 后端 businessStatus → 小程序数字状态 + */ + private Integer toAppStatus(String businessStatus) { + if (Func.isEmpty(businessStatus)) { + return null; + } + return switch (businessStatus) { + case STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING -> 0; + case STATUS_RUNNING -> 1; + case STATUS_COMPLETED -> 2; + case STATUS_CANCELLED -> 3; + default -> null; + }; + } + + private List splitCargoNames(String cargoName) { + if (Func.isEmpty(cargoName)) { + return Collections.emptyList(); + } + String normalized = cargoName.replace(",", ",").replace("、", ","); + List names = Func.toStrList(",", normalized).stream() + .map(String::trim) + .filter(Func::isNotEmpty) + .collect(Collectors.toList()); + return names.isEmpty() ? List.of(cargoName.trim()) : names; + } + + private String formatWeight(BigDecimal quantity, String unit) { + if (quantity == null) { + return ""; + } + String qty = quantity.stripTrailingZeros().toPlainString(); + return Func.isEmpty(unit) ? qty : qty + unit; + } + + private String formatDateTime(Date date) { + if (date == null) { + return ""; + } + return DateUtil.format(date, DateUtil.PATTERN_DATETIME); + } + + private String formatTimeRange(Waybill waybill) { + LocalDate start = waybill.getEstimatedStartTime() != null + ? waybill.getEstimatedStartTime() + : waybill.getStartDate(); + LocalDate end = waybill.getEstimatedEndTime() != null + ? waybill.getEstimatedEndTime() + : waybill.getEndDate(); + if (start == null && end == null) { + return ""; + } + if (start != null && end != null) { + if (start.getYear() == end.getYear()) { + return start.format(DATE_DOT) + " - " + end.format(DATE_DOT_SHORT); + } + return start.format(DATE_DOT) + " - " + end.format(DATE_DOT); + } + LocalDate only = start != null ? start : end; + return only.format(DATE_DOT); + } + + private BigDecimal resolveFreight(Waybill waybill) { + if (waybill.getUnitPrice() != null && waybill.getQuantity() != null) { + return waybill.getUnitPrice().multiply(waybill.getQuantity()).setScale(2, RoundingMode.HALF_UP); + } + return waybill.getOtherFeeTotal() == null ? BigDecimal.ZERO : waybill.getOtherFeeTotal(); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java index 6c4f7bd..008e827 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java @@ -37,15 +37,22 @@ import org.springblade.transport.mapper.ExceptionDisposalMapper; import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest; import org.springblade.transport.pojo.entity.ExceptionDisposal; import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord; +import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.ExceptionDisposalFollowRecordVO; import org.springblade.transport.pojo.vo.ExceptionDisposalVO; import org.springblade.transport.service.IExceptionDisposalService; +import org.springblade.transport.service.IWaybillService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * 异常处置服务实现类 @@ -62,9 +69,12 @@ public class ExceptionDisposalServiceImpl private static final String STATUS_COMPLETED = "completed"; private final ExceptionDisposalFollowRecordMapper followRecordMapper; + private final IWaybillService waybillService; - public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper) { + public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper, + IWaybillService waybillService) { this.followRecordMapper = followRecordMapper; + this.waybillService = waybillService; } @Override @@ -82,6 +92,79 @@ public class ExceptionDisposalServiceImpl return vo; } + @Override + @Transactional(rollbackFor = Exception.class) + public ExceptionDisposalVO submitReport(ExceptionDisposal request) { + if (request == null) { + throw new ServiceException("请填写异常信息"); + } + if (Func.isBlank(request.getExceptionType())) { + throw new ServiceException("请选择异常类型"); + } + if (Func.isBlank(request.getReportDescription())) { + throw new ServiceException("请填写上报说明"); + } + if (request.getReportDescription().length() > 500) { + throw new ServiceException("上报说明不能超过500字"); + } + + ExceptionDisposal disposal = new ExceptionDisposal(); + disposal.setExceptionType(request.getExceptionType().trim()); + disposal.setExceptionReason(Func.isBlank(request.getExceptionReason()) + ? null + : request.getExceptionReason().trim()); + disposal.setReportDescription(request.getReportDescription().trim()); + disposal.setScenePhotos(Func.isBlank(request.getScenePhotos()) + ? null + : request.getScenePhotos().trim()); + disposal.setDisposalStatus(STATUS_PENDING); + disposal.setReportTime(LocalDateTime.now()); + disposal.setReporterId(AuthUtil.getUserId()); + disposal.setReporterName(UserCache.getUserRealName(AuthUtil.getUserId())); + + fillFromWaybill(disposal, request.getWaybillId(), request.getWaybillNo()); + + if (disposal.getWaybillId() == null) { + throw new ServiceException("请关联运单后再上报"); + } + + if (!save(disposal)) { + throw new ServiceException("异常上报失败"); + } + return toVO(disposal); + } + + /** 按运单补齐运单号 / 车牌 / 项目 / 承运商等展示字段 */ + private void fillFromWaybill(ExceptionDisposal disposal, Long waybillId, String waybillNo) { + Waybill waybill = null; + if (waybillId != null) { + waybill = waybillService.getById(waybillId); + } + if (waybill == null && Func.isNotBlank(waybillNo)) { + waybill = waybillService.getOne(Wrappers.lambdaQuery() + .eq(Waybill::getWaybillNo, waybillNo) + .eq(Waybill::getIsDeleted, 0) + .last("LIMIT 1")); + } + if (waybill == null) { + if (waybillId != null || Func.isNotBlank(waybillNo)) { + throw new ServiceException("关联运单不存在"); + } + return; + } + disposal.setWaybillId(waybill.getId()); + disposal.setWaybillNo(waybill.getWaybillNo()); + disposal.setVehicleNo(waybill.getVehicleNo()); + disposal.setProjectId(waybill.getProjectId()); + disposal.setProjectName(waybill.getProjectName()); + disposal.setCarrierId(waybill.getCarrierId()); + disposal.setCarrierName(waybill.getCarrierName()); + String loadingOrMaster = Func.isNotBlank(waybill.getLoadingNo()) + ? waybill.getLoadingNo() + : waybill.getMasterNo(); + disposal.setLoadingOrMasterNo(loadingOrMaster); + } + @Override @Transactional(rollbackFor = Exception.class) public void follow(ExceptionDisposalFollowRequest request) { @@ -179,9 +262,49 @@ public class ExceptionDisposalServiceImpl vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); vo.setDisposalStatusName(statusName(entity.getDisposalStatus())); + vo.setScenePhotoList(splitPhotos(entity.getScenePhotos())); + fillRouteAndCargo(vo, entity.getWaybillId()); return vo; } + /** 按关联运单补齐详情页路线 / 货物展示字段 */ + private void fillRouteAndCargo(ExceptionDisposalVO vo, Long waybillId) { + if (vo == null || waybillId == null) { + return; + } + Waybill waybill = waybillService.getById(waybillId); + if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) { + return; + } + Map route = new HashMap<>(2); + route.put("start", Func.toStr(waybill.getDepartureName(), "")); + route.put("end", Func.toStr(waybill.getArrivalName(), "")); + vo.setRoute(route); + + Map cargo = new HashMap<>(2); + cargo.put("name", Func.toStr(waybill.getCargoName(), "")); + cargo.put("weight", formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())); + vo.setCargo(cargo); + } + + private String formatWeight(BigDecimal quantity, String unit) { + if (quantity == null) { + return ""; + } + String qty = quantity.stripTrailingZeros().toPlainString(); + return Func.isBlank(unit) ? qty : qty + unit; + } + + private List splitPhotos(String scenePhotos) { + if (Func.isBlank(scenePhotos)) { + return List.of(); + } + return Arrays.stream(scenePhotos.split(",")) + .map(String::trim) + .filter(s -> Func.isNotBlank(s)) + .collect(Collectors.toList()); + } + private List followRecords(Long id) { List records = followRecordMapper.selectList(Wrappers.lambdaQuery() .eq(ExceptionDisposalFollowRecord::getDisposalId, id) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProcessConfigServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProcessConfigServiceImpl.java index 131e546..a0015e8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProcessConfigServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProcessConfigServiceImpl.java @@ -33,7 +33,9 @@ import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; import org.springblade.transport.excel.ProcessConfigExportExcel; import org.springblade.transport.mapper.ProcessConfigMapper; +import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.pojo.entity.ProcessConfig; +import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.ProcessConfigVO; import org.springblade.transport.service.IProcessConfigService; @@ -43,8 +45,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; /** * 过程配置 服务实现类 @@ -54,15 +59,25 @@ import java.util.Objects; @Service public class ProcessConfigServiceImpl extends BaseServiceImpl implements IProcessConfigService { + private final WaybillMapper waybillMapper; + + public ProcessConfigServiceImpl(WaybillMapper waybillMapper) { + this.waybillMapper = waybillMapper; + } + @Override public IPage selectProcessConfigPage(IPage page, ProcessConfigVO processConfig) { IPage entityPage = page(page, buildQuery(processConfig)); - return ProcessConfigWrapper.build().pageVO(entityPage); + IPage voPage = ProcessConfigWrapper.build().pageVO(entityPage); + fillHasRelatedWaybill(voPage.getRecords()); + return voPage; } @Override public ProcessConfigVO detail(Long id) { - return ProcessConfigWrapper.build().entityVO(loadEditable(id, false)); + ProcessConfigVO detail = ProcessConfigWrapper.build().entityVO(loadEditable(id, false)); + fillHasRelatedWaybill(List.of(detail)); + return detail; } @Override @@ -71,6 +86,7 @@ public class ProcessConfigServiceImpl extends BaseServiceImpl records) { + if (Func.isEmpty(records)) { + return; + } + Set allProjectIds = new HashSet<>(); + for (ProcessConfigVO record : records) { + allProjectIds.addAll(parseProjectIds(record.getProjectIds())); + } + Set projectIdsWithWaybill = findProjectIdsWithWaybill(allProjectIds); + for (ProcessConfigVO record : records) { + List projectIds = parseProjectIds(record.getProjectIds()); + record.setHasRelatedWaybill(projectIds.stream().anyMatch(projectIdsWithWaybill::contains)); + } + } + + private boolean hasRelatedWaybill(String projectIds) { + return !findProjectIdsWithWaybill(new HashSet<>(parseProjectIds(projectIds))).isEmpty(); + } + + private Set findProjectIdsWithWaybill(Set projectIds) { + if (Func.isEmpty(projectIds)) { + return Set.of(); + } + Set result = new HashSet<>(); + for (Long projectId : projectIds) { + if (waybillMapper.selectCount(Wrappers.lambdaQuery() + .eq(Waybill::getProjectId, projectId) + .eq(Waybill::getIsDeleted, 0)) > 0) { + result.add(projectId); + } + } + return result; + } + + private List parseProjectIds(String projectIds) { + if (Func.isEmpty(projectIds)) { + return List.of(); + } + return Arrays.stream(projectIds.split(",")) + .map(String::trim) + .filter(Func::isNotEmpty) + .map(item -> { + try { + return Long.valueOf(item); + } catch (NumberFormatException ex) { + return null; + } + }) + .filter(Objects::nonNull) + .distinct() + .toList(); + } + private LambdaQueryWrapper buildQuery(ProcessConfigVO processConfig) { TransportBusinessSupport.validateAllDept(processConfig.getAllDept(), "过程配置"); LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery().eq(ProcessConfig::getIsDeleted, 0); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 688d1dd..966e186 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -36,15 +36,22 @@ import org.springblade.transport.excel.WaybillExcel; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Arrays; +import org.springblade.transport.mapper.WaybillEnroutePunchMapper; import org.springblade.transport.mapper.WaybillMapper; +import org.springblade.transport.mapper.WaybillNodePunchMapper; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ProcessConfig; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.entity.WaybillEnroutePunch; +import org.springblade.transport.pojo.entity.WaybillNodePunch; import org.springblade.transport.pojo.dto.WaybillMileageRequest; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; +import org.springblade.transport.pojo.vo.WaybillPunchPhotoVO; +import org.springblade.transport.pojo.vo.WaybillPunchRecordItemVO; +import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IProcessConfigService; @@ -53,6 +60,7 @@ import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.wrapper.WaybillWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,8 +68,11 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -94,9 +105,16 @@ public class WaybillServiceImpl extends BaseServiceImpl @org.springframework.context.annotation.Lazy private IReceivablePayableDetailService receivablePayableDetailService; + @jakarta.annotation.Resource + private WaybillNodePunchMapper waybillNodePunchMapper; + + @jakarta.annotation.Resource + private WaybillEnroutePunchMapper waybillEnroutePunchMapper; + @Override public IPage selectWaybillPage(IPage page, WaybillVO waybill) { IPage entityPage = page(page, buildQuery(waybill)); + entityPage.getRecords().forEach(this::syncDriverAcceptState); IPage result = WaybillWrapper.build().pageVO(entityPage); fillMileageMaintainable(result.getRecords()); return result; @@ -104,12 +122,327 @@ public class WaybillServiceImpl extends BaseServiceImpl @Override public WaybillVO detail(Long id) { - WaybillVO result = WaybillWrapper.build().entityVO(loadEditable(id, false)); + WaybillVO result = WaybillWrapper.build().entityVO(syncDriverAcceptState(loadEditable(id, false))); fillCustomerNameFromContract(result); fillMileageMaintainable(List.of(result)); return result; } + @Override + public WaybillPunchRecordsVO listPunchRecords(Long waybillId) { + WaybillPunchRecordsVO vo = new WaybillPunchRecordsVO(); + if (waybillId == null) { + return vo; + } + Waybill waybill = getById(waybillId); + if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) { + throw new ServiceException("运单不存在"); + } + String processJson = resolveLiveProcessJson(waybill); + Map> voucherTypesByNode = buildVoucherTypesIndex(processJson); + + List nodePunches = waybillNodePunchMapper.selectList(Wrappers.lambdaQuery() + .eq(WaybillNodePunch::getWaybillId, waybillId) + .orderByAsc(WaybillNodePunch::getPunchTime) + .orderByAsc(WaybillNodePunch::getId)); + Map latestNodePunch = new LinkedHashMap<>(); + for (WaybillNodePunch punch : nodePunches) { + String code = Func.toStr(punch.getNodeCode(), "").trim(); + String name = Func.toStr(punch.getNodeName(), "").trim(); + if (Func.isNotEmpty(code)) { + latestNodePunch.put(code.toLowerCase(Locale.ROOT), punch); + } + if (Func.isNotEmpty(name)) { + latestNodePunch.put(name.toLowerCase(Locale.ROOT), punch); + } + } + + List enroutePunches = waybillEnroutePunchMapper.selectList(Wrappers.lambdaQuery() + .eq(WaybillEnroutePunch::getWaybillId, waybillId) + .orderByAsc(WaybillEnroutePunch::getPunchTime) + .orderByAsc(WaybillEnroutePunch::getId)); + WaybillEnroutePunch latestEnroute = enroutePunches.isEmpty() ? null : enroutePunches.get(enroutePunches.size() - 1); + List transitTypes = voucherTypesByNode.getOrDefault("transit", List.of("货物照片")); + + List records = new ArrayList<>(); + List uploads = new ArrayList<>(); + List> processNodes = WaybillProcessSupport.listEnabledProcessNodes(processJson); + if (processNodes.isEmpty()) { + processNodes = WaybillProcessSupport.listDriverPunchNodes(processJson); + } + + for (Map node : processNodes) { + String nodeCode = WaybillProcessSupport.nodeKey(node); + String nodeName = WaybillProcessSupport.nodeName(node); + boolean isTransit = WaybillProcessSupport.isTransitNodePublic(node); + WaybillPunchRecordItemVO item = new WaybillPunchRecordItemVO(); + item.setNodeCode(nodeCode); + item.setNodeName(nodeName); + item.setType(isTransit ? "enroute" : "node"); + item.setExceptionFlag(false); + item.setPhotos(new ArrayList<>()); + + if (isTransit) { + if (latestEnroute != null) { + item.setId(latestEnroute.getId()); + item.setPunched(true); + item.setStatusName("已打卡"); + item.setPunchTime(formatPunchTime(latestEnroute.getPunchTime())); + item.setAddress(Func.toStr(latestEnroute.getAddress(), "")); + item.setLongitude(decimalText(latestEnroute.getLongitude())); + item.setLatitude(decimalText(latestEnroute.getLatitude())); + if (Func.isNotEmpty(latestEnroute.getPhoto())) { + String voucherType = transitTypes.isEmpty() ? "货物照片" : transitTypes.get(0); + WaybillPunchPhotoVO photo = buildPhoto( + nodeName, voucherType, latestEnroute.getPhoto().trim(), item.getPunchTime()); + item.getPhotos().add(photo); + } + // 司机上传:展示全部在途照片(不仅最新一条) + for (WaybillEnroutePunch punch : enroutePunches) { + if (Func.isEmpty(punch.getPhoto())) { + continue; + } + String voucherType = transitTypes.isEmpty() ? "货物照片" : transitTypes.get(0); + uploads.add(buildPhoto(nodeName, voucherType, punch.getPhoto().trim(), formatPunchTime(punch.getPunchTime()))); + } + } else { + item.setPunched(false); + item.setStatusName("未打卡"); + item.setPunchTime(""); + } + records.add(item); + continue; + } + + WaybillNodePunch punched = findLatestNodePunch(latestNodePunch, nodeCode, nodeName); + if (punched != null) { + List types = resolveNodeVoucherTypes(voucherTypesByNode, punched.getNodeCode(), nodeName); + item.setId(punched.getId()); + item.setPunched(true); + item.setStatusName("已打卡"); + item.setPunchTime(formatPunchTime(punched.getPunchTime())); + item.setAddress(Func.toStr(punched.getAddress(), "")); + item.setLongitude(decimalText(punched.getLongitude())); + item.setLatitude(decimalText(punched.getLatitude())); + item.setWeight(punched.getWeight()); + item.setVolume(punched.getVolume()); + item.setQuantity(punched.getQuantity()); + item.setRemark(punched.getRemark()); + item.setExceptionFlag(Objects.equals(punched.getExceptionFlag(), 1)); + List photos = decodePunchPhotos(punched.getPhotos(), nodeName, types, item.getPunchTime()); + item.setPhotos(photos); + uploads.addAll(photos); + } else { + item.setPunched(false); + item.setStatusName("未打卡"); + item.setPunchTime(""); + } + records.add(item); + } + + vo.setRecords(records); + vo.setDriverUploads(uploads); + return vo; + } + + private WaybillNodePunch findLatestNodePunch(Map index, String nodeCode, String nodeName) { + if (index == null || index.isEmpty()) { + return null; + } + if (Func.isNotEmpty(nodeCode)) { + WaybillNodePunch hit = index.get(nodeCode.toLowerCase(Locale.ROOT)); + if (hit != null) { + return hit; + } + } + if (Func.isNotEmpty(nodeName)) { + return index.get(nodeName.toLowerCase(Locale.ROOT)); + } + return null; + } + + /** 动态过程配置优先,回退运单快照 */ + private String resolveLiveProcessJson(Waybill waybill) { + if (waybill.getProjectId() != null) { + String projectId = String.valueOf(waybill.getProjectId()); + String live = processConfigService.list(Wrappers.lambdaQuery() + .eq(ProcessConfig::getStatus, 1) + .eq(ProcessConfig::getIsDeleted, 0) + .like(ProcessConfig::getProjectIds, projectId) + .orderByDesc(ProcessConfig::getUpdateTime) + .orderByDesc(ProcessConfig::getCreateTime)) + .stream() + .filter(cfg -> containsProjectId(cfg.getProjectIds(), projectId)) + .map(ProcessConfig::getNodeConfigJson) + .filter(Func::isNotEmpty) + .findFirst() + .orElse(null); + if (Func.isNotEmpty(live)) { + return live; + } + } + return waybill.getProcessJson(); + } + + private Map> buildVoucherTypesIndex(String processJson) { + Map> map = new LinkedHashMap<>(); + for (Map node : WaybillProcessSupport.listDriverPunchNodes(processJson)) { + String key = WaybillProcessSupport.nodeKey(node); + String name = WaybillProcessSupport.nodeName(node); + List types = WaybillProcessSupport.nodeStringList(node, "voucherTypes"); + if (Func.isNotEmpty(key)) { + map.put(key.toLowerCase(Locale.ROOT), types); + } + if (Func.isNotEmpty(name)) { + map.put(name.toLowerCase(Locale.ROOT), types); + } + } + return map; + } + + private List resolveNodeVoucherTypes(Map> index, String nodeCode, String nodeName) { + if (index == null || index.isEmpty()) { + return List.of(); + } + if (Func.isNotEmpty(nodeCode)) { + List hit = index.get(nodeCode.toLowerCase(Locale.ROOT)); + if (hit != null) { + return hit; + } + } + if (Func.isNotEmpty(nodeName)) { + List hit = index.get(nodeName.toLowerCase(Locale.ROOT)); + if (hit != null) { + return hit; + } + } + return List.of(); + } + + /** + * 解析打卡 photos: + * 1) JSON 数组 [{"type":"委托单","url":"..."}] + * 2) 逗号分隔 URL,按 voucherTypes 下标回推类型 + */ + @SuppressWarnings("unchecked") + private List decodePunchPhotos( + String raw, + String nodeName, + List voucherTypes, + String punchTime + ) { + List out = new ArrayList<>(); + if (Func.isEmpty(raw)) { + return out; + } + String text = raw.trim(); + if (text.startsWith("[")) { + try { + List list = JsonUtil.parse(text, List.class); + if (list != null) { + int i = 0; + for (Object item : list) { + if (item instanceof Map map) { + String url = Func.toStr(map.get("url"), "").trim(); + if (Func.isEmpty(url)) { + continue; + } + String type = Func.toStr(map.get("type"), "").trim(); + if (Func.isEmpty(type) && voucherTypes != null && i < voucherTypes.size()) { + type = voucherTypes.get(i); + } + if (Func.isEmpty(type)) { + type = "凭证" + (i + 1); + } + out.add(buildPhoto(nodeName, type, url, punchTime)); + i++; + } else if (item != null) { + String url = String.valueOf(item).trim(); + if (Func.isEmpty(url)) { + continue; + } + String type = (voucherTypes != null && i < voucherTypes.size()) + ? voucherTypes.get(i) + : ("凭证" + (i + 1)); + out.add(buildPhoto(nodeName, type, url, punchTime)); + i++; + } + } + return out; + } + } catch (Exception ignored) { + // fall through to comma split + } + } + String[] urls = text.split(","); + for (int i = 0; i < urls.length; i++) { + String url = urls[i].trim(); + if (Func.isEmpty(url)) { + continue; + } + String type = (voucherTypes != null && i < voucherTypes.size()) + ? voucherTypes.get(i) + : ("凭证" + (i + 1)); + out.add(buildPhoto(nodeName, type, url, punchTime)); + } + return out; + } + + private WaybillPunchPhotoVO buildPhoto(String nodeName, String voucherType, String url, String punchTime) { + WaybillPunchPhotoVO photo = new WaybillPunchPhotoVO(); + photo.setNodeName(nodeName); + photo.setVoucherType(voucherType); + photo.setUrl(url); + photo.setPunchTime(punchTime); + photo.setLabel(nodeName + "-" + voucherType); + return photo; + } + + private String formatPunchTime(Date time) { + if (time == null) { + return ""; + } + return org.springblade.core.tool.utils.DateUtil.format(time, org.springblade.core.tool.utils.DateUtil.PATTERN_DATETIME); + } + + private String decimalText(BigDecimal value) { + return value == null ? "" : value.stripTrailingZeros().toPlainString(); + } + + @Override + public Waybill syncDriverAcceptState(Waybill waybill) { + if (waybill == null || waybill.getId() == null) { + return waybill; + } + if (WaybillProcessSupport.isTerminalBusinessStatus(waybill.getBusinessStatus())) { + return waybill; + } + // 与司机端一致:优先项目最新过程配置,再回退运单快照 + String processJson = resolveLiveProcessJson(waybill); + if (Func.isNotEmpty(processJson)) { + waybill.setProcessJson(processJson); + } + boolean requireAccept = WaybillProcessSupport.requiresDriverAcceptConfirmation(processJson); + String acceptStatus = waybill.getDriverAcceptStatus(); + if (requireAccept && Func.isEmpty(acceptStatus)) { + acceptStatus = WaybillProcessSupport.ACCEPT_PENDING; + } + String nextStatus = WaybillProcessSupport.normalizeBusinessStatus( + waybill.getBusinessStatus(), processJson, acceptStatus); + boolean acceptChanged = !Objects.equals(acceptStatus, waybill.getDriverAcceptStatus()); + boolean statusChanged = !Objects.equals(nextStatus, waybill.getBusinessStatus()); + if (!acceptChanged && !statusChanged) { + return waybill; + } + waybill.setDriverAcceptStatus(acceptStatus); + waybill.setBusinessStatus(nextStatus); + update(Wrappers.lambdaUpdate() + .set(Waybill::getBusinessStatus, nextStatus) + .set(Waybill::getDriverAcceptStatus, acceptStatus) + .eq(Waybill::getId, waybill.getId())); + return waybill; + } + @Override @Transactional(rollbackFor = Exception.class) public boolean submit(Waybill waybill) { @@ -128,20 +461,31 @@ public class WaybillServiceImpl extends BaseServiceImpl private void prepareForSave(Waybill waybill) { boolean created = Func.isEmpty(waybill.getId()); + Waybill oldRecord = null; if (!created) { - Waybill oldRecord = loadEditable(waybill.getId(), true); + oldRecord = loadEditable(waybill.getId(), true); assertNotLoaded(oldRecord); waybill.setWaybillNo(oldRecord.getWaybillNo()); waybill.setLoadingNo(oldRecord.getLoadingNo()); waybill.setDeptId(oldRecord.getDeptId()); waybill.setDeptName(oldRecord.getDeptName()); waybill.setMileageRemark(oldRecord.getMileageRemark()); + if (Func.isEmpty(waybill.getProcessJson())) { + waybill.setProcessJson(oldRecord.getProcessJson()); + } } else { waybill.setLoadingNo(null); waybill.setMileageRemark(null); - fillProjectProcessConfig(waybill); } + // 无过程快照时按项目回填;再按接单设置决定 pending / running + fillProjectProcessConfig(waybill); prepare(waybill); + if (oldRecord != null) { + preserveOrResetDriverAccept(waybill, oldRecord); + } else { + clearDriverAcceptRecord(waybill); + } + applyDriverAcceptBusinessStatus(waybill); fillCustomerName(waybill); if (created && Func.isEmpty(waybill.getWaybillNo())) { waybill.setWaybillNo(nextCode()); @@ -180,21 +524,60 @@ public class WaybillServiceImpl extends BaseServiceImpl } private void fillProjectProcessConfig(Waybill waybill) { - if (Func.isNotEmpty(waybill.getProcessJson()) || Func.isEmpty(waybill.getProjectId())) { + String live = resolveLiveProcessJson(waybill); + if (Func.isNotEmpty(live)) { + waybill.setProcessJson(live); + } + } + + /** + * 无过程配置,或接单设置为「无需确认接单」时,运单直接进入进行中(running); + * 需要司机确认接单且尚未接单时保持待执行(pending)。 + */ + private void applyDriverAcceptBusinessStatus(Waybill waybill) { + if (WaybillProcessSupport.isTerminalBusinessStatus(waybill.getBusinessStatus())) { return; } - String projectId = String.valueOf(waybill.getProjectId()); - processConfigService.list(Wrappers.lambdaQuery() - .eq(ProcessConfig::getStatus, 1) - .eq(ProcessConfig::getIsDeleted, 0) - .like(ProcessConfig::getProjectIds, projectId) - .orderByDesc(ProcessConfig::getCreateTime)) - .stream() - .filter(processConfig -> containsProjectId(processConfig.getProjectIds(), projectId)) - .map(ProcessConfig::getNodeConfigJson) - .filter(Func::isNotEmpty) - .findFirst() - .ifPresent(waybill::setProcessJson); + String processJson = resolveLiveProcessJson(waybill); + if (Func.isNotEmpty(processJson)) { + waybill.setProcessJson(processJson); + } + boolean requireAccept = WaybillProcessSupport.requiresDriverAcceptConfirmation(processJson); + if (requireAccept && Func.isEmpty(waybill.getDriverAcceptStatus())) { + waybill.setDriverAcceptStatus(WaybillProcessSupport.ACCEPT_PENDING); + } + waybill.setBusinessStatus(WaybillProcessSupport.normalizeBusinessStatus( + waybill.getBusinessStatus(), processJson, waybill.getDriverAcceptStatus())); + } + + private void preserveOrResetDriverAccept(Waybill waybill, Waybill oldRecord) { + if (driverAssignmentChanged(waybill, oldRecord)) { + clearDriverAcceptRecord(waybill); + return; + } + waybill.setDriverAcceptStatus(oldRecord.getDriverAcceptStatus()); + waybill.setDriverAcceptTime(oldRecord.getDriverAcceptTime()); + waybill.setDriverAcceptDriverId(oldRecord.getDriverAcceptDriverId()); + waybill.setDriverRejectTime(oldRecord.getDriverRejectTime()); + waybill.setDriverRejectReason(oldRecord.getDriverRejectReason()); + } + + private boolean driverAssignmentChanged(Waybill waybill, Waybill oldRecord) { + return !Objects.equals(waybill.getDriverId(), oldRecord.getDriverId()) + || !Objects.equals( + TransportBusinessSupport.trimToNull(waybill.getDriverPhone()), + TransportBusinessSupport.trimToNull(oldRecord.getDriverPhone())) + || !Objects.equals( + TransportBusinessSupport.trimToNull(waybill.getVehicleNo()), + TransportBusinessSupport.trimToNull(oldRecord.getVehicleNo())); + } + + private void clearDriverAcceptRecord(Waybill waybill) { + waybill.setDriverAcceptStatus(null); + waybill.setDriverAcceptTime(null); + waybill.setDriverAcceptDriverId(null); + waybill.setDriverRejectTime(null); + waybill.setDriverRejectReason(null); } private boolean containsProjectId(String projectIds, String projectId) { @@ -254,7 +637,10 @@ public class WaybillServiceImpl extends BaseServiceImpl excel.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())); excel.setCreateTime(record.getCreateTime()); excel.setUpdateTime(record.getUpdateTime()); - excel.setBusinessStatus(WaybillWrapper.businessStatusName(record.getBusinessStatus())); + String processJson = resolveLiveProcessJson(record); + excel.setBusinessStatus(WaybillWrapper.businessStatusName( + WaybillProcessSupport.normalizeBusinessStatus( + record.getBusinessStatus(), processJson, record.getDriverAcceptStatus()))); return excel; }).toList(); } @@ -352,8 +738,10 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setRemark(source.getRemark()); target.setBusinessStatus("pending"); target.setWaybillNo(nextCode()); + clearDriverAcceptRecord(target); fillProjectProcessConfig(target); prepare(target); + applyDriverAcceptBusinessStatus(target); validate(target); save(target); return detail(target.getId()); @@ -418,20 +806,61 @@ public class WaybillServiceImpl extends BaseServiceImpl @Override @Transactional(rollbackFor = Exception.class) - public boolean reassign(Long id) { - Waybill waybill = loadEditable(id, true); - assertNotLoaded(waybill); - if (!"pending".equals(waybill.getBusinessStatus())) { - throw new ServiceException("仅待执行运单允许重新派单"); + public boolean reassign(Waybill request) { + if (request == null || Func.isEmpty(request.getId())) { + throw new ServiceException("运单ID不能为空"); } - waybill.setBusinessStatus("pending"); + Waybill waybill = loadEditable(request.getId(), true); + assertNotLoaded(waybill); + if (!"pending".equals(waybill.getBusinessStatus()) && !"running".equals(waybill.getBusinessStatus())) { + throw new ServiceException("仅待执行/进行中运单允许重新派单"); + } + String driverName = TransportBusinessSupport.trimToNull(request.getDriverName()); + String driverPhone = TransportBusinessSupport.trimToNull(request.getDriverPhone()); + String vehicleNo = TransportBusinessSupport.trimToNull(request.getVehicleNo()); + TransportBusinessSupport.validateRequired(driverName, "司机不能为空"); + TransportBusinessSupport.validateRequired(driverPhone, "手机号不能为空"); + TransportBusinessSupport.validateRequired(vehicleNo, "车牌号不能为空"); + + waybill.setDriverId(request.getDriverId()); + waybill.setDriverName(driverName); + waybill.setDriverPhone(driverPhone); + waybill.setVehicleNo(vehicleNo); + // 同步承运/任务 JSON,避免列表与表单读到旧司机 + waybill.setCarrierJson(buildImportCarrierJson(waybill)); + waybill.setTaskInfoJson(buildTaskInfoJson(waybill)); + + fillProjectProcessConfig(waybill); + clearDriverAcceptRecord(waybill); + // 清空后重新进入待接单 + waybill.setDriverAcceptStatus(WaybillProcessSupport.ACCEPT_PENDING); + applyDriverAcceptBusinessStatus(waybill); return updateById(waybill); } @Override @Transactional(rollbackFor = Exception.class) public boolean complete(Long id) { - Waybill waybill = loadEditable(id, true); + return doComplete(loadEditable(id, true)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean completeWithoutDeptCheck(Long id) { + if (Func.isEmpty(id)) { + throw new ServiceException("运单ID不能为空"); + } + Waybill waybill = getById(id); + if (Func.isEmpty(waybill) || Objects.equals(waybill.getIsDeleted(), 1)) { + throw new ServiceException("运单不存在"); + } + return doComplete(waybill); + } + + /** + * 完成运单核心逻辑:状态改为 completed,并检查生成应收应付明细。 + */ + private boolean doComplete(Waybill waybill) { if ("completed".equals(waybill.getBusinessStatus()) || "cancelled".equals(waybill.getBusinessStatus())) { throw new ServiceException("当前状态不允许完成"); } @@ -682,6 +1111,8 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setCarrierJson(TransportBusinessSupport.trimToNull(waybill.getCarrierJson())); waybill.setTaskInfoJson(TransportBusinessSupport.trimToNull(waybill.getTaskInfoJson())); waybill.setProcessJson(TransportBusinessSupport.trimToNull(waybill.getProcessJson())); + waybill.setDriverAcceptStatus(TransportBusinessSupport.trimToNull(waybill.getDriverAcceptStatus())); + waybill.setDriverRejectReason(TransportBusinessSupport.trimToNull(waybill.getDriverRejectReason())); waybill.setRouteJson(TransportBusinessSupport.trimToNull(waybill.getRouteJson())); waybill.setFreightJson(TransportBusinessSupport.trimToNull(waybill.getFreightJson())); waybill.setAttachmentsJson(TransportBusinessSupport.trimToNull(waybill.getAttachmentsJson())); @@ -694,6 +1125,7 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setDeptName(dept.getDeptName()); } if (waybill.getStatus() == null) { waybill.setStatus(1); } + // 默认 pending;最终 pending/running 由 applyDriverAcceptBusinessStatus 按过程配置校正 if (Func.isEmpty(waybill.getBusinessStatus())) { waybill.setBusinessStatus("pending"); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java new file mode 100644 index 0000000..6e8236f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java @@ -0,0 +1,472 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support; + +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Func; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 运单过程配置解析(对齐 web 端 waybill-manage / process-config) + */ +public final class WaybillProcessSupport { + + public static final String STATUS_PENDING = "pending"; + public static final String STATUS_RUNNING = "running"; + public static final String ACCEPT_PENDING = "pending"; + public static final String ACCEPT_ACCEPTED = "accepted"; + public static final String ACCEPT_REJECTED = "rejected"; + private static final String CONFIRM_YES = "yes"; + private static final String CONFIRM_NO_ACCEPT = "no_confirm_accept"; + private static final String NODE_TRANSIT = "transit"; + private static final String NODE_TRANSIT_NAME = "在途"; + private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("H:mm"); + private static final DateTimeFormatter HM_PADDED = DateTimeFormatter.ofPattern("HH:mm"); + + private WaybillProcessSupport() { + } + + /** + * 在途打卡判定结果(供司机端「今日在途打卡」面板使用)。 + */ + public record TransitCheckinDecision( + boolean punchEnabled, + boolean visible, + boolean dueToday, + boolean doneToday, + int frequencyDays, + String timeStart, + String timeEnd + ) { + public static TransitCheckinDecision hidden() { + return new TransitCheckinDecision(false, false, false, false, 1, "00:00", "23:59"); + } + } + + /** + * 是否需要接单确认。 + *

+ * 存在启用的接单节点,且 confirmMode=yes(是否确认=是)即为需要接单; + * 不依赖 confirmDriver(是否勾选司机)——只要尚未接单或已拒绝,业务状态均为待执行。 + *

+ * 无过程配置 / 接单节点为「无需确认接单」→ false。 + */ + public static boolean requiresDriverAcceptConfirmation(String processJson) { + List> nodes = parseProcessNodes(processJson); + if (nodes.isEmpty()) { + return false; + } + for (Map node : nodes) { + if (!isAcceptNode(node) || !isEnabled(node)) { + continue; + } + String confirmMode = stringVal(node.get("confirmMode")); + if (CONFIRM_NO_ACCEPT.equals(confirmMode)) { + return false; + } + if (CONFIRM_YES.equals(confirmMode) || Func.isEmpty(confirmMode)) { + return true; + } + } + return false; + } + + /** + * 根据过程配置决定司机侧初始业务状态: + * 需要确认接单 → pending(待执行/待接单);否则 → running(进行中)。 + */ + public static String resolveDriverFacingStatus(String processJson) { + return requiresDriverAcceptConfirmation(processJson) ? STATUS_PENDING : STATUS_RUNNING; + } + + public static boolean isAccepted(String driverAcceptStatus) { + return ACCEPT_ACCEPTED.equalsIgnoreCase(stringVal(driverAcceptStatus)); + } + + public static boolean isRejected(String driverAcceptStatus) { + return ACCEPT_REJECTED.equalsIgnoreCase(stringVal(driverAcceptStatus)); + } + + public static boolean isTerminalBusinessStatus(String businessStatus) { + return "draft".equals(businessStatus) + || "completed".equals(businessStatus) + || "cancelled".equals(businessStatus) + || "waiting_dispatch".equals(businessStatus) + || "dispatching".equals(businessStatus); + } + + /** + * 校正业务状态。 + *

+ * 需要接单(接单节点 confirmMode=yes)且尚未接单(未响应 / 已拒绝)→ pending(待执行); + * 已接单 → running(进行中);不需要接单 → running(仅当当前为空或 pending 时提升)。 + * draft / completed / cancelled 等终态或调度中间态不改动。 + */ + public static String normalizeBusinessStatus(String businessStatus, String processJson) { + return normalizeBusinessStatus(businessStatus, processJson, null); + } + + public static String normalizeBusinessStatus(String businessStatus, String processJson, String driverAcceptStatus) { + if (isTerminalBusinessStatus(businessStatus)) { + return businessStatus; + } + if (requiresDriverAcceptConfirmation(processJson)) { + return isAccepted(driverAcceptStatus) ? STATUS_RUNNING : STATUS_PENDING; + } + if (Func.isEmpty(businessStatus) || STATUS_PENDING.equals(businessStatus)) { + return STATUS_RUNNING; + } + return businessStatus; + } + + /** + * 过程配置是否启用在途打卡:在途节点 enabled 且 punch=是。 + */ + public static boolean isTransitPunchEnabled(String processJson) { + Map transit = findTransitNode(processJson); + return transit != null && isEnabled(transit) && isTruthy(transit.get("punch")); + } + + /** + * 计算「今日在途打卡」是否展示 / 是否到期。 + *

+ * 规则: + *

    + *
  • 在途节点未启用或 punch≠是 → 不展示
  • + *
  • 运单非进行中(running)→ 不展示
  • + *
  • 频次:每 N 天打卡 1 次;无历史 → 到期;上次打卡日 + N ≤ 今日 → 到期
  • + *
  • 时段:到期时须落在 timeStart~timeEnd(支持跨午夜);今日已打则仍展示(已打卡态)
  • + *
+ */ + public static TransitCheckinDecision evaluateTransitCheckin( + String processJson, + String businessStatus, + Date lastPunchAt, + LocalDateTime now + ) { + Map transit = findTransitNode(processJson); + if (transit == null || !isEnabled(transit) || !isTruthy(transit.get("punch"))) { + return TransitCheckinDecision.hidden(); + } + int frequencyDays = parsePositiveInt(transit.get("frequencyDays"), 1); + String timeStart = normalizeHm(stringVal(transit.get("timeStart")), "00:00"); + String timeEnd = normalizeHm(stringVal(transit.get("timeEnd")), "23:59"); + if (!STATUS_RUNNING.equals(businessStatus)) { + return new TransitCheckinDecision(true, false, false, false, frequencyDays, timeStart, timeEnd); + } + + LocalDateTime current = now == null ? LocalDateTime.now() : now; + LocalDate today = current.toLocalDate(); + LocalDate lastDate = toLocalDate(lastPunchAt); + boolean doneToday = lastDate != null && lastDate.equals(today); + boolean dueToday; + if (lastDate == null) { + dueToday = true; + } else { + LocalDate nextDue = lastDate.plusDays(frequencyDays); + dueToday = !today.isBefore(nextDue); + } + boolean inWindow = isWithinTimeWindow(current.toLocalTime(), timeStart, timeEnd); + boolean visible = doneToday || (dueToday && inWindow); + return new TransitCheckinDecision(true, visible, dueToday && inWindow && !doneToday, doneToday, frequencyDays, timeStart, timeEnd); + } + + public static Map findTransitNode(String processJson) { + List> nodes = parseProcessNodes(processJson); + for (Map node : nodes) { + if (isTransitNode(node)) { + return node; + } + } + return null; + } + + /** + * 司机端应展示的打卡节点:enabled 且 punch=是,排除接单/回单。 + */ + public static List> listDriverPunchNodes(String processJson) { + List> result = new ArrayList<>(); + for (Map node : parseProcessNodes(processJson)) { + if (!isEnabled(node) || !isTruthy(node.get("punch"))) { + continue; + } + if (isAcceptNode(node) || isReturnNode(node)) { + continue; + } + result.add(node); + } + return result; + } + + /** + * 启用中的过程节点(按配置顺序,含接单/回单)。 + */ + public static List> listEnabledProcessNodes(String processJson) { + List> result = new ArrayList<>(); + for (Map node : parseProcessNodes(processJson)) { + if (isEnabled(node)) { + result.add(node); + } + } + return result; + } + + /** + * 当前过程节点在启用节点列表中的下标;找不到返回 0(视为从首个开始)。 + */ + public static int indexOfCurrentNode(List> enabledNodes, String currentProcessNode) { + if (enabledNodes == null || enabledNodes.isEmpty()) { + return 0; + } + String current = stringVal(currentProcessNode); + if (Func.isEmpty(current)) { + // 无当前节点:定位到第一个非接单节点 + for (int i = 0; i < enabledNodes.size(); i++) { + if (!isAcceptNode(enabledNodes.get(i))) { + return i; + } + } + return 0; + } + for (int i = 0; i < enabledNodes.size(); i++) { + Map node = enabledNodes.get(i); + String key = stringVal(node.get("key")); + String name = stringVal(node.get("name")); + if (current.equalsIgnoreCase(key) || current.equals(name) || name.contains(current) || current.contains(name)) { + return i; + } + } + return 0; + } + + public static boolean isTransitNodePublic(Map node) { + return isTransitNode(node); + } + + public static boolean nodeNeedLocation(Map node) { + return isTruthy(node.get("location")); + } + + public static boolean nodeNeedCargo(Map node) { + return isTruthy(node.get("uploadCargo")); + } + + public static boolean nodeNeedVoucher(Map node) { + return isTruthy(node.get("uploadVoucher")); + } + + @SuppressWarnings("unchecked") + public static List nodeStringList(Map node, String field) { + Object raw = node.get(field); + if (raw instanceof List list) { + List out = new ArrayList<>(); + for (Object item : list) { + if (item != null && Func.isNotEmpty(String.valueOf(item).trim())) { + out.add(String.valueOf(item).trim()); + } + } + return out; + } + if (raw instanceof String str && Func.isNotEmpty(str)) { + String[] parts = str.split("[,,]"); + List out = new ArrayList<>(); + for (String part : parts) { + if (Func.isNotEmpty(part.trim())) { + out.add(part.trim()); + } + } + return out; + } + return Collections.emptyList(); + } + + public static String nodeKey(Map node) { + return stringVal(node.get("key")); + } + + public static String nodeName(Map node) { + String name = stringVal(node.get("name")); + return Func.isEmpty(name) ? nodeKey(node) : name; + } + + @SuppressWarnings("unchecked") + public static List> parseProcessNodes(String processJson) { + if (Func.isEmpty(processJson)) { + return Collections.emptyList(); + } + try { + Object parsed = JsonUtil.parse(processJson, Object.class); + if (parsed instanceof List list) { + return castNodeList(list); + } + if (parsed instanceof Map map) { + Object nodes = map.get("nodes"); + if (nodes instanceof List list) { + return castNodeList(list); + } + Object nodeConfigJson = map.get("nodeConfigJson"); + if (nodeConfigJson instanceof String str && Func.isNotEmpty(str)) { + return parseProcessNodes(str); + } + if (nodeConfigJson instanceof List list) { + return castNodeList(list); + } + } + } catch (Exception ignored) { + return Collections.emptyList(); + } + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + private static List> castNodeList(List list) { + return list.stream() + .filter(Map.class::isInstance) + .map(item -> (Map) item) + .toList(); + } + + private static boolean isReturnNode(Map node) { + String key = stringVal(node.get("key")); + String name = stringVal(node.get("name")); + return "return".equals(key) || "回单".equals(name); + } + + private static boolean isAcceptNode(Map node) { + String key = stringVal(node.get("key")); + String name = stringVal(node.get("name")); + return "accept".equals(key) || "接单".equals(name); + } + + private static boolean isTransitNode(Map node) { + String key = stringVal(node.get("key")); + String name = stringVal(node.get("name")); + String type = stringVal(node.get("type")); + return NODE_TRANSIT.equals(key) || NODE_TRANSIT_NAME.equals(name) || NODE_TRANSIT.equals(type); + } + + private static boolean isEnabled(Map node) { + Object enabled = node.get("enabled"); + if (enabled == null) { + return true; + } + if (enabled instanceof Boolean bool) { + return bool; + } + String text = String.valueOf(enabled).trim(); + return !("false".equalsIgnoreCase(text) || "0".equals(text)); + } + + private static boolean isTruthy(Object value) { + if (value == null) { + return false; + } + if (value instanceof Boolean bool) { + return bool; + } + String text = String.valueOf(value).trim(); + return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text); + } + + private static String stringVal(Object value) { + return value == null ? "" : String.valueOf(value).trim(); + } + + private static int parsePositiveInt(Object value, int defaultVal) { + if (value == null) { + return defaultVal; + } + try { + int n = Integer.parseInt(String.valueOf(value).trim()); + return n < 1 ? defaultVal : n; + } catch (NumberFormatException ex) { + return defaultVal; + } + } + + private static String normalizeHm(String value, String fallback) { + LocalTime t = parseHm(value); + if (t == null) { + return fallback; + } + return t.format(HM_PADDED); + } + + private static LocalTime parseHm(String value) { + if (Func.isEmpty(value)) { + return null; + } + String text = value.trim(); + try { + return LocalTime.parse(text, HM_PADDED); + } catch (DateTimeParseException ignored) { + // fallthrough + } + try { + return LocalTime.parse(text, HM); + } catch (DateTimeParseException ignored) { + return null; + } + } + + /** + * 是否在打卡时段内(按 HH:mm 分钟含端点);timeStart > timeEnd 视为跨午夜。 + */ + public static boolean isWithinTimeWindow(LocalTime now, String timeStart, String timeEnd) { + LocalTime start = parseHm(timeStart); + LocalTime end = parseHm(timeEnd); + if (start == null || end == null || now == null) { + return true; + } + int nowM = now.getHour() * 60 + now.getMinute(); + int startM = start.getHour() * 60 + start.getMinute(); + int endM = end.getHour() * 60 + end.getMinute(); + if (startM == endM) { + return true; + } + if (startM < endM) { + return nowM >= startM && nowM <= endM; + } + // 跨午夜:如 22:00-06:00 + return nowM >= startM || nowM <= endM; + } + + private static LocalDate toLocalDate(Date date) { + if (date == null) { + return null; + } + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java index 8b9d066..64ea4aa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java @@ -30,6 +30,7 @@ import org.springblade.system.cache.UserCache; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.support.WaybillProcessSupport; import java.util.Objects; @@ -52,7 +53,11 @@ public class WaybillWrapper extends BaseEntityWrapper { waybillVO.setDataSource(TransportBusinessSupport.normalizeWaybillDataSource(waybill.getDataSource())); Long currentDeptId = Func.firstLong(AuthUtil.getDeptId()); waybillVO.setReadonly(currentDeptId != null && !Objects.equals(waybill.getDeptId(), currentDeptId)); - waybillVO.setBusinessStatusName(businessStatusName(waybill.getBusinessStatus())); + String displayStatus = WaybillProcessSupport.normalizeBusinessStatus( + waybill.getBusinessStatus(), waybill.getProcessJson(), waybill.getDriverAcceptStatus()); + waybillVO.setBusinessStatus(displayStatus); + waybillVO.setBusinessStatusName(businessStatusName(displayStatus)); + waybillVO.setRequireAccept(WaybillProcessSupport.requiresDriverAcceptConfirmation(waybill.getProcessJson())); return waybillVO; } diff --git a/doc/sql/transport/blade_tms_business.sql b/doc/sql/transport/blade_tms_business.sql index 16d2341..62a0472 100644 --- a/doc/sql/transport/blade_tms_business.sql +++ b/doc/sql/transport/blade_tms_business.sql @@ -254,6 +254,11 @@ CREATE TABLE `blade_waybill` ( `driver_id` bigint(20) DEFAULT NULL COMMENT '司机ID', `driver_name` varchar(100) DEFAULT NULL COMMENT '司机姓名', `driver_phone` varchar(50) DEFAULT NULL COMMENT '司机手机号', + `driver_accept_status` varchar(32) DEFAULT NULL COMMENT '司机接单状态:pending待接单/accepted已接单/rejected已拒绝', + `driver_accept_time` datetime DEFAULT NULL COMMENT '司机接单时间', + `driver_accept_driver_id` bigint(20) DEFAULT NULL COMMENT '接单司机ID', + `driver_reject_time` datetime DEFAULT NULL COMMENT '司机拒绝接单时间', + `driver_reject_reason` varchar(200) DEFAULT NULL COMMENT '司机拒绝接单原因', `vehicle_no` varchar(100) DEFAULT NULL COMMENT '车/船/航班/班列号', `captain_name` varchar(20) DEFAULT NULL COMMENT '船长', `cabin_no` varchar(30) DEFAULT NULL COMMENT '舱位', diff --git a/doc/sql/transport/blade_waybill_driver_accept_20260911.sql b/doc/sql/transport/blade_waybill_driver_accept_20260911.sql new file mode 100644 index 0000000..bab3405 --- /dev/null +++ b/doc/sql/transport/blade_waybill_driver_accept_20260911.sql @@ -0,0 +1,6 @@ +ALTER TABLE `blade_waybill` + ADD COLUMN `driver_accept_status` varchar(32) DEFAULT NULL COMMENT '司机接单状态:pending待接单/accepted已接单/rejected已拒绝' AFTER `driver_phone`, + ADD COLUMN `driver_accept_time` datetime DEFAULT NULL COMMENT '司机接单时间' AFTER `driver_accept_status`, + ADD COLUMN `driver_accept_driver_id` bigint(20) DEFAULT NULL COMMENT '接单司机ID' AFTER `driver_accept_time`, + ADD COLUMN `driver_reject_time` datetime DEFAULT NULL COMMENT '司机拒绝接单时间' AFTER `driver_accept_driver_id`, + ADD COLUMN `driver_reject_reason` varchar(200) DEFAULT NULL COMMENT '司机拒绝接单原因' AFTER `driver_reject_time`; diff --git a/doc/sql/transport/blade_waybill_enroute_punch_20260911.sql b/doc/sql/transport/blade_waybill_enroute_punch_20260911.sql new file mode 100644 index 0000000..702ec61 --- /dev/null +++ b/doc/sql/transport/blade_waybill_enroute_punch_20260911.sql @@ -0,0 +1,23 @@ +-- 运单在途打卡记录(过程配置 transit 节点 punch=是) +CREATE TABLE IF NOT EXISTS `blade_waybill_enroute_punch` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID', + `waybill_id` bigint(20) NOT NULL COMMENT '运单ID', + `waybill_no` varchar(64) DEFAULT NULL COMMENT '运单号', + `driver_id` bigint(20) DEFAULT NULL COMMENT '打卡司机ID', + `punch_time` datetime NOT NULL COMMENT '打卡时间', + `longitude` decimal(12, 8) DEFAULT NULL COMMENT '经度', + `latitude` decimal(12, 8) DEFAULT NULL COMMENT '纬度', + `address` varchar(500) DEFAULT NULL COMMENT '打卡地址', + `photo` varchar(1000) DEFAULT NULL COMMENT '货物照片URL', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_enroute_punch_waybill` (`waybill_id`, `punch_time`) USING BTREE, + KEY `idx_enroute_punch_driver` (`driver_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运单在途打卡记录'; diff --git a/doc/sql/transport/blade_waybill_node_punch_20260911.sql b/doc/sql/transport/blade_waybill_node_punch_20260911.sql new file mode 100644 index 0000000..f595a23 --- /dev/null +++ b/doc/sql/transport/blade_waybill_node_punch_20260911.sql @@ -0,0 +1,30 @@ +-- 运单过程节点打卡记录(到场/装货/发货/到货/卸货/签收等 punch=是,不含在途) +CREATE TABLE IF NOT EXISTS `blade_waybill_node_punch` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID', + `waybill_id` bigint(20) NOT NULL COMMENT '运单ID', + `waybill_no` varchar(64) DEFAULT NULL COMMENT '运单号', + `driver_id` bigint(20) DEFAULT NULL COMMENT '打卡司机ID', + `node_code` varchar(64) NOT NULL COMMENT '过程节点 key', + `node_name` varchar(64) DEFAULT NULL COMMENT '过程节点名称', + `punch_time` datetime NOT NULL COMMENT '打卡时间', + `longitude` decimal(12, 8) DEFAULT NULL COMMENT '经度', + `latitude` decimal(12, 8) DEFAULT NULL COMMENT '纬度', + `address` varchar(500) DEFAULT NULL COMMENT '打卡地址', + `photos` varchar(2000) DEFAULT NULL COMMENT '凭证照片URL,多张逗号分隔', + `weight` varchar(32) DEFAULT NULL COMMENT '重量(吨)', + `volume` varchar(32) DEFAULT NULL COMMENT '体积(方)', + `quantity` varchar(32) DEFAULT NULL COMMENT '数量(件)', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `exception_flag` int(11) DEFAULT '0' COMMENT '是否异常:0否 1是', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_node_punch_waybill` (`waybill_id`, `node_code`, `punch_time`) USING BTREE, + KEY `idx_node_punch_driver` (`driver_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运单过程节点打卡记录'; From 2da4d824fddfb2764fd9bd1902f4d9970f4fb3a2 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 11 Sep 2026 15:30:22 +0800 Subject: [PATCH 089/114] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/vo/DriverWaybillCardVO.java | 2 +- .../impl/DriverWaybillServiceImpl.java | 8 +++-- .../support/WaybillProcessSupport.java | 32 +++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java index 1b87f5d..162b386 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java @@ -109,7 +109,7 @@ public class DriverWaybillCardVO implements Serializable { @Schema(description = "过程配置是否启用在途打卡(在途节点 punch=是)") private Boolean transitPunchEnabled; - @Schema(description = "是否展示「今日在途打卡」面板(到期且在时段内,或今日已打)") + @Schema(description = "是否展示「今日在途打卡」面板(过程配置 punch=是即展示;频次/时段只影响 requireTransitCheckinToday)") private Boolean transitCheckinVisible; @Schema(description = "今日是否需要在途打卡(到期且未打且在时段内)") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index 2071910..ad1cbc1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -716,12 +716,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { vo.setDefaultExpanded(false); if (isTransit) { - // 在途:今日已打则不可再打,回显最新一次信息 - boolean punchOn = transit != null && transit.punchEnabled(); + // 在途:过程配置 punch=是即下发可见卡;今日已打则不可再打,回显最新一次 + // 在途不采集货量;货物照片仅当过程配置勾选上传凭证时下发 + boolean punchOn = (transit != null && transit.punchEnabled()) + || WaybillProcessSupport.isTruthyPublic(cfg.get("punch")); boolean done = transit != null && transit.doneToday(); if (!punchOn) { continue; } + vo.setNeedCargo(false); + vo.setCargoTypes(Collections.emptyList()); vo.setVisible(true); vo.setDone(done); vo.setActionable(!done); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java index 6e8236f..8814002 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java @@ -164,9 +164,10 @@ public final class WaybillProcessSupport { * 规则: *
    *
  • 在途节点未启用或 punch≠是 → 不展示
  • - *
  • 运单非进行中(running)→ 不展示
  • + *
  • 过程配置 punch=是 → 始终展示折叠卡(与频次/时段解耦,对齐「所有打卡=是的节点都显示」)
  • + *
  • 频次 / 时段仅影响 dueToday(今日是否仍需打),不隐藏卡片
  • *
  • 频次:每 N 天打卡 1 次;无历史 → 到期;上次打卡日 + N ≤ 今日 → 到期
  • - *
  • 时段:到期时须落在 timeStart~timeEnd(支持跨午夜);今日已打则仍展示(已打卡态)
  • + *
  • 时段:到期时须落在 timeStart~timeEnd(支持跨午夜)
  • *
*/ public static TransitCheckinDecision evaluateTransitCheckin( @@ -182,24 +183,25 @@ public final class WaybillProcessSupport { int frequencyDays = parsePositiveInt(transit.get("frequencyDays"), 1); String timeStart = normalizeHm(stringVal(transit.get("timeStart")), "00:00"); String timeEnd = normalizeHm(stringVal(transit.get("timeEnd")), "23:59"); + // punch=是即展示卡片;非进行中仅不可作为「今日待打」 if (!STATUS_RUNNING.equals(businessStatus)) { - return new TransitCheckinDecision(true, false, false, false, frequencyDays, timeStart, timeEnd); + return new TransitCheckinDecision(true, true, false, false, frequencyDays, timeStart, timeEnd); } LocalDateTime current = now == null ? LocalDateTime.now() : now; LocalDate today = current.toLocalDate(); LocalDate lastDate = toLocalDate(lastPunchAt); boolean doneToday = lastDate != null && lastDate.equals(today); - boolean dueToday; + boolean dueByFrequency; if (lastDate == null) { - dueToday = true; + dueByFrequency = true; } else { LocalDate nextDue = lastDate.plusDays(frequencyDays); - dueToday = !today.isBefore(nextDue); + dueByFrequency = !today.isBefore(nextDue); } boolean inWindow = isWithinTimeWindow(current.toLocalTime(), timeStart, timeEnd); - boolean visible = doneToday || (dueToday && inWindow); - return new TransitCheckinDecision(true, visible, dueToday && inWindow && !doneToday, doneToday, frequencyDays, timeStart, timeEnd); + boolean dueToday = dueByFrequency && inWindow && !doneToday; + return new TransitCheckinDecision(true, true, dueToday, doneToday, frequencyDays, timeStart, timeEnd); } public static Map findTransitNode(String processJson) { @@ -274,6 +276,11 @@ public final class WaybillProcessSupport { return isTransitNode(node); } + /** 供业务层判断过程配置布尔字段(兼容 true/1/yes/是) */ + public static boolean isTruthyPublic(Object value) { + return isTruthy(value); + } + public static boolean nodeNeedLocation(Map node) { return isTruthy(node.get("location")); } @@ -395,8 +402,15 @@ public final class WaybillProcessSupport { if (value instanceof Boolean bool) { return bool; } + if (value instanceof Number number) { + return number.intValue() != 0; + } String text = String.valueOf(value).trim(); - return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text); + return "true".equalsIgnoreCase(text) + || "1".equals(text) + || "yes".equalsIgnoreCase(text) + || "y".equalsIgnoreCase(text) + || "是".equals(text); } private static String stringVal(Object value) { From 2262c39f60f6dd275a49811af09f27a92f583fdf Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 13 Sep 2026 01:01:09 +0800 Subject: [PATCH 090/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/entity/ProjectApply.java | 4 + .../transport/pojo/vo/ProjectApplyVO.java | 21 ++ .../controller/ProjectApplyController.java | 17 + .../service/IProjectApplyService.java | 3 + .../service/impl/ProjectApplyServiceImpl.java | 311 +++++++++++++++++- ...e_project_apply_change_record_20260912.sql | 3 + .../blade_project_contract_management.sql | 1 + 7 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 doc/sql/transport/blade_project_apply_change_record_20260912.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java index 8ba12f4..5526da8 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java @@ -170,6 +170,10 @@ public class ProjectApply extends TenantEntity { @Schema(description = "项目附件JSON") private String attachmentsJson; + @Schema(description = "变更记录JSON") + @TableField(value = "change_record_json", insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private String changeRecordJson; + @Schema(description = "审批状态") private String approvalStatus; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java index 8431d07..8ac54d1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java @@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.ProjectApply; import java.io.Serial; +import java.math.BigDecimal; /** * 项目立项视图实体类 @@ -79,4 +80,24 @@ public class ProjectApplyVO extends ProjectApply { @Schema(description = "是否仅查询可用于临时额度申请的项目") private Boolean temporaryCreditLimitSelectable; + @TableField(exist = false) + @Schema(description = "资金使用风险等级:high、medium、none") + private String fundUseRisk; + + @TableField(exist = false) + @Schema(description = "资金使用风险名称") + private String fundUseRiskName; + + @TableField(exist = false) + @Schema(description = "资金使用率(百分比)") + private BigDecimal fundUseRate; + + @TableField(exist = false) + @Schema(description = "已使用资金金额") + private BigDecimal usedFundLimit; + + @TableField(exist = false) + @Schema(description = "风险计算额度基数") + private BigDecimal maxFundLimit; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java index 41e399d..4cb6918 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java @@ -48,6 +48,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; /** * 项目立项 控制器 @@ -70,6 +71,22 @@ public class ProjectApplyController extends BladeController { return R.data(projectApplyService.detail(id)); } + @GetMapping("/fund-risk-stats") + @ApiOperationSupport(order = 2) + @Operation(summary = "资金使用风险统计", description = "传入项目筛选条件") + public R> fundRiskStats(ProjectApplyVO projectApply) { + return R.data(projectApplyService.fundRiskStats(projectApply)); + } + + @GetMapping("/change-record/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "变更记录详情", description = "传入项目ID和变更记录序号") + public R> changeRecordDetail( + @Parameter(description = "项目ID", required = true) @RequestParam Long id, + @Parameter(description = "变更记录序号,从0开始", required = true) @RequestParam Integer recordIndex) { + return R.data(projectApplyService.changeRecordDetail(id, recordIndex)); + } + @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页", description = "传入projectApply") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java index ee3cd16..6facb4c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java @@ -29,6 +29,7 @@ import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.vo.ProjectApplyVO; import java.util.List; +import java.util.Map; /** * 项目立项 服务类 @@ -39,6 +40,8 @@ public interface IProjectApplyService extends BaseService { IPage selectProjectApplyPage(IPage page, ProjectApplyVO projectApply); ProjectApplyVO detail(Long id); + Map fundRiskStats(ProjectApplyVO projectApply); + Map changeRecordDetail(Long id, Integer recordIndex); boolean saveDraft(ProjectApply projectApply); boolean submit(ProjectApply projectApply); boolean submitApproval(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index 08802b5..fcc93ae 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -25,16 +25,24 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; import org.springblade.transport.excel.ProjectApplyExcel; import org.springblade.transport.mapper.ProjectApplyMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.ReceiptClaimSettlementMapper; +import org.springblade.transport.mapper.TemporaryCreditLimitMapper; import org.springblade.transport.pojo.entity.ProjectApply; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.ReceiptClaimSettlement; +import org.springblade.transport.pojo.entity.TemporaryCreditLimit; import org.springblade.transport.pojo.vo.ProjectApplyVO; import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.support.TransportBusinessSupport; @@ -47,9 +55,14 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * 项目立项 服务实现类 @@ -57,6 +70,7 @@ import java.util.Objects; * @author Chill */ @Service +@RequiredArgsConstructor public class ProjectApplyServiceImpl extends BaseServiceImpl implements IProjectApplyService { private static final String STATUS_DRAFT = "draft"; @@ -68,15 +82,21 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl selectProjectApplyPage(IPage page, ProjectApplyVO projectApply) { IPage entityPage = page(page, buildQuery(projectApply)); IPage voPage = ProjectApplyWrapper.build().pageVO(entityPage); + fillFundUseRisk(voPage.getRecords()); voPage.getRecords().forEach(this::fillReadonly); return voPage; } @@ -89,25 +109,64 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl fundRiskStats(ProjectApplyVO projectApply) { + projectApply.setFundUseRisk(null); + List records = list(buildQuery(projectApply)).stream() + .map(record -> ProjectApplyWrapper.build().entityVO(record)) + .toList(); + fillFundUseRisk(records); + Map stats = new LinkedHashMap<>(); + stats.put("high", (int) records.stream().filter(item -> "high".equals(item.getFundUseRisk())).count()); + stats.put("medium", (int) records.stream().filter(item -> "medium".equals(item.getFundUseRisk())).count()); + stats.put("none", (int) records.stream().filter(item -> "none".equals(item.getFundUseRisk())).count()); + return stats; + } + + @Override + public Map changeRecordDetail(Long id, Integer recordIndex) { + if (recordIndex == null || recordIndex < 0) { + throw new ServiceException("变更记录序号不能为空"); + } + ProjectApply projectApply = loadExists(id); + List> records = parseChangeRecords(projectApply.getChangeRecordJson()); + if (recordIndex >= records.size()) { + throw new ServiceException("变更记录不存在"); + } + return records.get(recordIndex); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean saveDraft(ProjectApply projectApply) { - prepare(projectApply); + ProjectApply beforeRecord = null; if (Func.isNotEmpty(projectApply.getId())) { - loadEditable(projectApply.getId(), true); + beforeRecord = loadEditable(projectApply.getId(), true); } + prepare(projectApply); prepareCreateOrUpdate(projectApply); projectApply.setApprovalStatus(resolveEditableStatus(projectApply)); + if (beforeRecord != null) { + projectApply.setChangeRecordJson(beforeRecord.getChangeRecordJson()); + } + if (beforeRecord != null && Objects.equals(beforeRecord.getApprovalStatus(), STATUS_DRAFT)) { + appendProjectChangeRecord(projectApply, CHANGE_TYPE_DRAFT, null, STATUS_DRAFT, "草稿", + buildProjectChangeSnapshot(beforeRecord), buildProjectChangeSnapshot(projectApply)); + } return saveOrUpdate(projectApply); } @Override @Transactional(rollbackFor = Exception.class) public boolean submit(ProjectApply projectApply) { + ProjectApply beforeRecord = null; + if (Func.isNotEmpty(projectApply.getId())) { + beforeRecord = loadEditable(projectApply.getId(), true); + } prepare(projectApply); validateSubmit(projectApply); - if (Func.isNotEmpty(projectApply.getId())) { - loadEditable(projectApply.getId(), true); + if (beforeRecord != null) { + projectApply.setChangeRecordJson(beforeRecord.getChangeRecordJson()); } prepareCreateOrUpdate(projectApply); projectApply.setApprovalStatus(STATUS_REVIEWING); @@ -186,10 +245,13 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl beforeData = buildProjectChangeSnapshot(oldRecord); prepare(projectApply); copyChangeFields(oldRecord, projectApply); validateSubmit(oldRecord); validateChangeLength(oldRecord); + Map afterData = buildProjectChangeSnapshot(oldRecord); + appendProjectChangeRecord(oldRecord, CHANGE_TYPE_PROJECT, oldRecord.getChangeReason(), "saved", "已保存", beforeData, afterData); return updateById(oldRecord); } @@ -197,10 +259,14 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl beforeData = buildProjectChangeSnapshot(oldRecord); prepare(projectApply); copyChangeFields(oldRecord, projectApply); validateSubmit(oldRecord); validateChange(oldRecord, projectApply.getChangeType()); + Map afterData = buildProjectChangeSnapshot(oldRecord); + appendProjectChangeRecord(oldRecord, projectApply.getChangeType(), oldRecord.getChangeReason(), + STATUS_CHANGE_REVIEWING, "变更审批中", beforeData, afterData); oldRecord.setApprovalStatus(STATUS_CHANGE_REVIEWING); oldRecord.setCurrentNode(projectApply.getChangeType() + "审批"); oldRecord.setCurrentProcessor("待处理"); @@ -271,7 +337,10 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl riskRecords = baseMapper.selectList(queryWrapper).stream() + .map(record -> ProjectApplyWrapper.build().entityVO(record)) + .toList(); + fillFundUseRisk(riskRecords); + List matchedIds = riskRecords.stream() + .filter(record -> Objects.equals(record.getFundUseRisk(), projectApply.getFundUseRisk())) + .map(ProjectApply::getId) + .toList(); + queryWrapper.in(ProjectApply::getId, matchedIds.isEmpty() ? List.of(-1L) : matchedIds); + } return queryWrapper; } + private void fillFundUseRisk(List records) { + if (records == null || records.isEmpty()) { + return; + } + List projectIds = records.stream() + .map(ProjectApply::getId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (projectIds.isEmpty()) { + return; + } + + Map approvedTemporaryLimits = new HashMap<>(); + temporaryCreditLimitMapper.selectList(Wrappers.lambdaQuery() + .in(TemporaryCreditLimit::getProjectId, projectIds) + .eq(TemporaryCreditLimit::getApprovalStatus, STATUS_SETTLEMENT_APPROVED) + .eq(TemporaryCreditLimit::getIsDeleted, 0)) + .forEach(item -> approvedTemporaryLimits.merge(item.getProjectId(), nonNegative(item.getApplyLimit()), BigDecimal::add)); + + List approvedReceivables = formalSettlementMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlement::getProjectId, projectIds) + .eq(FormalSettlement::getSettlementType, "receivable") + .eq(FormalSettlement::getApprovalStatus, STATUS_SETTLEMENT_APPROVED) + .eq(FormalSettlement::getIsDeleted, 0)); + Map settlementProjects = approvedReceivables.stream() + .filter(item -> item.getId() != null && item.getProjectId() != null) + .collect(Collectors.toMap(FormalSettlement::getId, FormalSettlement::getProjectId, (left, right) -> left)); + Map usedFundLimits = new HashMap<>(); + if (!settlementProjects.isEmpty()) { + receiptClaimSettlementMapper.selectList(Wrappers.lambdaQuery() + .in(ReceiptClaimSettlement::getFormalSettlementId, settlementProjects.keySet()) + .eq(ReceiptClaimSettlement::getStatus, 1) + .eq(ReceiptClaimSettlement::getIsDeleted, 0)) + .forEach(item -> { + Long projectId = settlementProjects.get(item.getFormalSettlementId()); + if (projectId != null) { + usedFundLimits.merge(projectId, nonNegative(item.getAllocatedReceiptAmount()), BigDecimal::add); + } + }); + } + + records.forEach(project -> { + BigDecimal projectLimit = nonNegative(project.getFundLimit()); + BigDecimal temporaryLimit = approvedTemporaryLimits.getOrDefault(project.getId(), BigDecimal.ZERO); + BigDecimal maxFundLimit = projectLimit.max(temporaryLimit); + BigDecimal usedFundLimit = usedFundLimits.getOrDefault(project.getId(), BigDecimal.ZERO); + BigDecimal fundUseRate = maxFundLimit.signum() == 0 + ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) + : usedFundLimit.multiply(BigDecimal.valueOf(100)).divide(maxFundLimit, 2, RoundingMode.HALF_UP); + String risk = fundUseRate.compareTo(new BigDecimal("90")) >= 0 ? "high" + : fundUseRate.compareTo(new BigDecimal("80")) >= 0 ? "medium" : "none"; + project.setUsedFundLimit(usedFundLimit); + project.setMaxFundLimit(maxFundLimit); + project.setFundUseRate(fundUseRate); + project.setFundUseRisk(risk); + project.setFundUseRiskName("high".equals(risk) ? "高风险" : "medium".equals(risk) ? "中风险" : "无风险"); + }); + } + + private BigDecimal nonNegative(BigDecimal value) { + return value == null || value.signum() < 0 ? BigDecimal.ZERO : value; + } + private void prepareCreateOrUpdate(ProjectApply projectApply) { if (Func.isEmpty(projectApply.getId())) { // 新增项目不产生变更记录,避免客户端误传变更字段导致记录回显。 @@ -414,6 +558,146 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl buildProjectChangeSnapshot(ProjectApply projectApply) { + Map data = new LinkedHashMap<>(); + data.put("projectType", projectApply.getProjectType()); + data.put("projectName", projectApply.getProjectName()); + data.put("projectShortName", projectApply.getProjectShortName()); + data.put("businessDeptName", projectApply.getBusinessDeptName()); + data.put("undertakeDeptName", projectApply.getUndertakeDeptName()); + data.put("projectSource", projectApply.getProjectSource()); + data.put("sourceRemark", projectApply.getSourceRemark()); + data.put("fundLimit", projectApply.getFundLimit()); + data.put("receivableLimit", projectApply.getReceivableLimit()); + data.put("receivableDays", projectApply.getReceivableDays()); + data.put("paymentDays", projectApply.getPaymentDays()); + data.put("cargoType", projectApply.getCargoType()); + data.put("cargoQuantity", projectApply.getCargoQuantity()); + data.put("businessStartDate", projectApply.getBusinessStartDate()); + data.put("businessEndDate", projectApply.getBusinessEndDate()); + data.put("transportRoute", projectApply.getTransportRoute()); + data.put("transportType", projectApply.getTransportType()); + data.put("businessType", projectApply.getBusinessType()); + data.put("businessMode", projectApply.getBusinessMode()); + data.put("projectScale", projectApply.getProjectScale()); + data.put("settlementMode", projectApply.getSettlementMode()); + data.put("estimatedProfit", projectApply.getEstimatedProfit()); + data.put("profitRate", projectApply.getProfitRate()); + data.put("fundDemand", projectApply.getFundDemand()); + data.put("handlerUserName", projectApply.getHandlerUserName()); + data.put("principalUserName", projectApply.getPrincipalUserName()); + data.put("customerNames", projectApply.getCustomerNames()); + data.put("carrierNames", projectApply.getCarrierNames()); + data.put("situationRemark", normalizeProjectSnapshotJson(projectApply.getSituationRemark())); + data.put("attachmentsJson", normalizeProjectSnapshotJson(projectApply.getAttachmentsJson())); + return data; + } + + private String normalizeProjectSnapshotJson(String value) { + if (Func.isEmpty(value)) { + return null; + } + String text = value.trim(); + try { + Object parsed = JsonUtil.parse(text, Object.class); + if (parsed instanceof List list && list.isEmpty()) { + return null; + } + if (parsed instanceof Map map && map.values().stream().allMatch(this::isEmptyProjectSnapshotValue)) { + return null; + } + } catch (Exception ignored) { + // 非JSON文本按原值参与差异比较。 + } + return text; + } + + private boolean isEmptyProjectSnapshotValue(Object value) { + if (value == null) { + return true; + } + if (value instanceof String text) { + return text.isBlank(); + } + if (value instanceof List list) { + return list.isEmpty(); + } + if (value instanceof Map map) { + return map.isEmpty(); + } + return false; + } + + private void appendProjectChangeRecord(ProjectApply projectApply, String changeType, String changeReason, + String status, String statusName, Map beforeData, + Map afterData) { + retainChangedSnapshotFields(beforeData, afterData); + if (beforeData.isEmpty()) { + return; + } + List> records = parseChangeRecords(projectApply.getChangeRecordJson()); + Map record = new LinkedHashMap<>(); + record.put("changeDate", LocalDate.now().toString()); + record.put("handlerUserId", AuthUtil.getUserId()); + record.put("handlerUserName", AuthUtil.getUserName()); + record.put("changeType", changeType); + record.put("changeContent", buildProjectChangeContent(beforeData, afterData)); + record.put("changeReason", TransportBusinessSupport.trimToNull(changeReason)); + record.put("status", status); + record.put("statusName", statusName); + record.put("changedFields", new ArrayList<>(beforeData.keySet())); + record.put("beforeData", beforeData); + record.put("afterData", afterData); + records.add(record); + projectApply.setChangeRecordJson(JsonUtil.toJson(records)); + } + + private void retainChangedSnapshotFields(Map beforeData, Map afterData) { + List unchangedFields = beforeData.entrySet().stream() + .filter(entry -> Objects.equals(entry.getValue(), afterData.get(entry.getKey()))) + .map(Map.Entry::getKey) + .toList(); + unchangedFields.forEach(field -> { + beforeData.remove(field); + afterData.remove(field); + }); + } + + private String buildProjectChangeContent(Map beforeData, Map afterData) { + return beforeData.keySet().stream() + .map(field -> "【" + projectChangeFieldLabel(field) + "】从【" + + formatProjectChangeValue(beforeData.get(field)) + "】调整为【" + + formatProjectChangeValue(afterData.get(field)) + "】") + .collect(Collectors.joining(";")); + } + + private String projectChangeFieldLabel(String field) { + Map labels = Map.ofEntries( + Map.entry("projectType", "项目类型"), Map.entry("projectName", "项目名称"), + Map.entry("projectShortName", "项目简称"), Map.entry("businessDeptName", "业务部门"), + Map.entry("undertakeDeptName", "承办部门"), Map.entry("projectSource", "项目由来"), + Map.entry("sourceRemark", "项目由来说明"), Map.entry("fundLimit", "项目资金使用额度"), + Map.entry("receivableLimit", "项目应收账款额度"), Map.entry("receivableDays", "应收账款回款期限"), + Map.entry("paymentDays", "回款账期"), Map.entry("cargoType", "货物类型"), + Map.entry("cargoQuantity", "预估货物数量"), Map.entry("businessStartDate", "业务周期起"), + Map.entry("businessEndDate", "业务周期止"), Map.entry("transportRoute", "运输线路"), + Map.entry("transportType", "运输类型"), Map.entry("businessType", "业务类型"), + Map.entry("businessMode", "业务模式"), Map.entry("projectScale", "项目规模"), + Map.entry("settlementMode", "结算方式"), Map.entry("estimatedProfit", "预计利润"), + Map.entry("profitRate", "利润率"), Map.entry("fundDemand", "履约保证金"), + Map.entry("handlerUserName", "项目经办人"), Map.entry("principalUserName", "项目负责人"), + Map.entry("customerNames", "客户名称"), Map.entry("carrierNames", "下游承运商"), + Map.entry("situationRemark", "项目情况说明"), Map.entry("attachmentsJson", "项目材料")); + return labels.getOrDefault(field, field); + } + + private String formatProjectChangeValue(Object value) { + if (value == null || (value instanceof String text && text.isBlank())) { + return "空"; + } + return String.valueOf(value); + } + private void validateDraft(ProjectApply projectApply) { TransportBusinessSupport.validateRequired(projectApply.getProjectType(), "请选择项目类型"); TransportBusinessSupport.validateRequired(projectApply.getProjectName(), "请输入项目名称"); @@ -563,6 +847,25 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl> parseChangeRecords(String value) { + if (Func.isEmpty(value)) { + return new ArrayList<>(); + } + try { + Object records = JsonUtil.parse(value, Object.class); + if (records instanceof List list) { + return (List>) (List) list; + } + if (records instanceof Map map) { + return new ArrayList<>(List.of((Map) map)); + } + } catch (Exception ignored) { + // 历史记录JSON损坏时从空列表继续,避免影响项目保存。 + } + return new ArrayList<>(); + } + private void fillReadonly(ProjectApplyVO projectApplyVO) { projectApplyVO.setReadonly(!canCurrentUserOperate(projectApplyVO)); } diff --git a/doc/sql/transport/blade_project_apply_change_record_20260912.sql b/doc/sql/transport/blade_project_apply_change_record_20260912.sql new file mode 100644 index 0000000..6c497a3 --- /dev/null +++ b/doc/sql/transport/blade_project_apply_change_record_20260912.sql @@ -0,0 +1,3 @@ +-- 项目立项增加变更记录JSON +ALTER TABLE `blade_project_apply` + ADD COLUMN `change_record_json` text DEFAULT NULL COMMENT '变更记录JSON' AFTER `attachments_json`; diff --git a/doc/sql/transport/blade_project_contract_management.sql b/doc/sql/transport/blade_project_contract_management.sql index 07920d2..caa7ebd 100644 --- a/doc/sql/transport/blade_project_contract_management.sql +++ b/doc/sql/transport/blade_project_contract_management.sql @@ -49,6 +49,7 @@ CREATE TABLE `blade_project_apply` ( `carrier_json` text DEFAULT NULL COMMENT '承运商信息JSON', `situation_remark` text DEFAULT NULL COMMENT '项目情况说明', `attachments_json` text DEFAULT NULL COMMENT '项目附件JSON', + `change_record_json` text DEFAULT NULL COMMENT '变更记录JSON', `approval_status` varchar(50) DEFAULT NULL COMMENT '审批状态', `current_node` varchar(100) DEFAULT NULL COMMENT '当前节点', `current_processor` varchar(100) DEFAULT NULL COMMENT '当前处理人', From 940f0b9b9f2b7df5a00cbbf938e2a2bacb42b538 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 14 Sep 2026 01:49:02 +0800 Subject: [PATCH 091/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E3=80=81=E8=BF=90=E5=8A=9B=E3=80=81=E8=BD=A6?= =?UTF-8?q?=E8=88=B9=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/pojo/entity/MeasurementUnit.java | 65 + .../system/pojo/vo/MeasurementUnitVO.java | 51 + .../pojo/entity/VehicleDispatch.java | 68 + .../transport/pojo/vo/VehicleDispatchVO.java | 58 + .../controller/MeasurementUnitController.java | 137 ++ .../system/mapper/MeasurementUnitMapper.java | 47 + .../system/mapper/MeasurementUnitMapper.xml | 55 + .../service/IMeasurementUnitService.java | 61 + .../impl/MeasurementUnitServiceImpl.java | 142 ++ .../wrapper/MeasurementUnitWrapper.java | 51 + .../controller/VehicleDispatchController.java | 105 ++ .../transport/excel/VehicleDispatchExcel.java | 52 + .../transport/mapper/AccidentRecordMapper.xml | 4 +- .../mapper/AnnualInspectionRecordMapper.xml | 4 +- .../CreditScoreQuantificationMapper.xml | 4 +- .../mapper/CustomerArchiveMapper.xml | 4 +- .../mapper/InsuranceRecordMapper.xml | 4 +- .../mapper/MaintenancePlanMapper.xml | 4 +- .../mapper/MaintenanceRecordMapper.xml | 4 +- .../transport/mapper/MileageRecordMapper.xml | 4 +- .../mapper/OilElectricRecordMapper.xml | 4 +- .../mapper/OtherExpenseRecordMapper.xml | 4 +- .../mapper/TireReplacementRecordMapper.xml | 4 +- .../mapper/TransportChangeRecordMapper.xml | 4 +- .../mapper/VehicleDispatchMapper.java | 31 + .../mapper/VehicleDispatchMapper.xml | 46 + .../mapper/ViolationRecordMapper.xml | 4 +- .../service/IVehicleDispatchService.java | 29 + .../impl/VehicleDispatchServiceImpl.java | 127 ++ .../wrapper/VehicleDispatchWrapper.java | 49 + doc/sql/bladex/bladex.mysql.all.create.sql | 32 + doc/sql/transport/blade_measurement_unit.sql | 31 + doc/sql/transport/blade_vehicle_dispatch.sql | 44 + hs_err_pid25098.log | 1375 +++++++++++++++++ 34 files changed, 2682 insertions(+), 26 deletions(-) create mode 100644 blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java create mode 100644 blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/MeasurementUnitVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VehicleDispatch.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/controller/MeasurementUnitController.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/service/IMeasurementUnitService.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MeasurementUnitWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VehicleDispatchController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVehicleDispatchService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VehicleDispatchServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/VehicleDispatchWrapper.java create mode 100644 doc/sql/transport/blade_measurement_unit.sql create mode 100644 doc/sql/transport/blade_vehicle_dispatch.sql create mode 100644 hs_err_pid25098.log diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java new file mode 100644 index 0000000..ad80a1e --- /dev/null +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java @@ -0,0 +1,65 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.entity; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.mp.base.BaseEntity; + +import java.io.Serial; + +/** + * 计量单位实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_measurement_unit") +@Schema(description = "计量单位") +public class MeasurementUnit extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 计量单位 + */ + @Schema(description = "计量单位") + private String unitName; + + /** + * 计量维度 + */ + @Schema(description = "计量维度") + private String dimension; + + /** + * 备注 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + @Schema(description = "备注") + private String remark; + +} diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/MeasurementUnitVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/MeasurementUnitVO.java new file mode 100644 index 0000000..adaa6b3 --- /dev/null +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/MeasurementUnitVO.java @@ -0,0 +1,51 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.system.pojo.entity.MeasurementUnit; + +import java.io.Serial; + +/** + * 计量单位视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "计量单位") +public class MeasurementUnitVO extends MeasurementUnit { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + + @TableField(exist = false) + @Schema(description = "更新人姓名") + private String updateUserName; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VehicleDispatch.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VehicleDispatch.java new file mode 100644 index 0000000..ffba700 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VehicleDispatch.java @@ -0,0 +1,68 @@ +/** + * 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.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** + * 车辆调度申请实体类。 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_vehicle_dispatch") +@Schema(description = "车辆调度申请") +public class VehicleDispatch extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "申请单号") + private String applicationNo; + @Schema(description = "车牌号") + private String plateNo; + @Schema(description = "所属组织") + private String organizationName; + @Schema(description = "使用部门") + private String useDepartment; + @Schema(description = "审批状态:draft/reviewing/rejected/approved") + private String approvalStatus; + @Schema(description = "当前节点") + private String currentNode; + @Schema(description = "当前处理人") + private String currentProcessor; + @Schema(description = "备注") + private String remark; + @Schema(description = "附件JSON") + private String attachments; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java new file mode 100644 index 0000000..05ed2f9 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java @@ -0,0 +1,58 @@ +/** + * 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.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.VehicleDispatch; + +import java.io.Serial; + +/** + * 车辆调度申请视图对象。 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "车辆调度申请") +public class VehicleDispatchVO extends VehicleDispatch { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField(exist = false) + @Schema(description = "审批状态名称") + private String approvalStatusName; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) + @Schema(description = "更新人姓名") + private String updateUserName; +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/MeasurementUnitController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/MeasurementUnitController.java new file mode 100644 index 0000000..341311f --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/MeasurementUnitController.java @@ -0,0 +1,137 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.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.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +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.annotation.PreAuth; +import org.springblade.core.tenant.annotation.NonDS; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.pojo.entity.MeasurementUnit; +import org.springblade.system.pojo.vo.MeasurementUnitVO; +import org.springblade.system.service.IMeasurementUnitService; +import org.springblade.system.wrapper.MeasurementUnitWrapper; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 计量单位控制器 + * + * @author Chill + */ +@NonDS +@RestController +@AllArgsConstructor +@PreAuth(menu = "measurement_unit") +@RequestMapping("/measurement-unit") +@Tag(name = "计量单位", description = "计量单位") +public class MeasurementUnitController extends BladeController { + + private final IMeasurementUnitService measurementUnitService; + + /** + * 详情 + * + * @param measurementUnit 查询条件 + * @return 计量单位详情 + */ + @GetMapping("/detail") + @ApiOperationSupport(order = 1) + @Operation(summary = "详情", description = "传入measurementUnit") + public R detail(MeasurementUnit measurementUnit) { + MeasurementUnit detail = measurementUnitService.getOne(Condition.getQueryWrapper(measurementUnit)); + if (detail == null) { + return R.fail("数据不存在"); + } + return R.data(MeasurementUnitWrapper.build().entityVO(detail)); + } + + /** + * 分页 + * + * @param measurementUnit 查询条件 + * @param query 分页参数 + * @return 计量单位分页 + */ + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页", description = "传入measurementUnit") + public R> list(MeasurementUnitVO measurementUnit, Query query) { + IPage pages = measurementUnitService.selectMeasurementUnitPage( + Condition.getPage(query), measurementUnit + ); + return R.data(pages); + } + + /** + * 新增或修改 + * + * @param measurementUnit 计量单位 + * @return 操作结果 + */ + @PostMapping("/submit") + @ApiOperationSupport(order = 3) + @Operation(summary = "新增或修改", description = "传入measurementUnit") + public R submit(@Valid @RequestBody MeasurementUnit measurementUnit) { + return R.status(measurementUnitService.submit(measurementUnit)); + } + + /** + * 删除 + * + * @param ids 主键集合 + * @return 操作结果 + */ + @PostMapping("/remove") + @ApiOperationSupport(order = 4) + @Operation(summary = "逻辑删除", description = "传入ids") + public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { + return R.status(measurementUnitService.deleteLogic(Func.toLongList(ids))); + } + + /** + * 启用或停用 + * + * @param id 主键 + * @param status 状态 + * @return 操作结果 + */ + @PostMapping("/status") + @ApiOperationSupport(order = 5) + @Operation(summary = "启用或停用", description = "传入id和status") + public R status(@Parameter(description = "主键", required = true) @RequestParam Long id, + @Parameter(description = "状态", required = true) @RequestParam Integer status) { + return R.status(measurementUnitService.changeStatus(id, status)); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java new file mode 100644 index 0000000..e4b9897 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java @@ -0,0 +1,47 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Param; +import org.springblade.system.pojo.entity.MeasurementUnit; +import org.springblade.system.pojo.vo.MeasurementUnitVO; + +import java.util.List; + +/** + * 计量单位 Mapper 接口 + * + * @author Chill + */ +public interface MeasurementUnitMapper extends BaseMapper { + + /** + * 自定义分页 + * + * @param page 分页参数 + * @param measurementUnit 查询参数 + * @return 计量单位分页 + */ + List selectMeasurementUnitPage(IPage page, + @Param("measurementUnit") MeasurementUnitVO measurementUnit); + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml new file mode 100644 index 0000000..387f1a2 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMeasurementUnitService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMeasurementUnitService.java new file mode 100644 index 0000000..f7e2f7b --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMeasurementUnitService.java @@ -0,0 +1,61 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.system.pojo.entity.MeasurementUnit; +import org.springblade.system.pojo.vo.MeasurementUnitVO; + +/** + * 计量单位服务类 + * + * @author Chill + */ +public interface IMeasurementUnitService extends BaseService { + + /** + * 自定义分页 + * + * @param page 分页参数 + * @param measurementUnit 查询参数 + * @return 计量单位分页 + */ + IPage selectMeasurementUnitPage(IPage page, + MeasurementUnitVO measurementUnit); + + /** + * 新增或修改计量单位 + * + * @param measurementUnit 计量单位 + * @return 是否成功 + */ + boolean submit(MeasurementUnit measurementUnit); + + /** + * 启用或停用计量单位 + * + * @param id 主键 + * @param status 状态 + * @return 是否成功 + */ + boolean changeStatus(Long id, Integer status); + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java new file mode 100644 index 0000000..615bb67 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java @@ -0,0 +1,142 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.mapper.MeasurementUnitMapper; +import org.springblade.system.pojo.entity.MeasurementUnit; +import org.springblade.system.pojo.vo.MeasurementUnitVO; +import org.springblade.system.service.IMeasurementUnitService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; +import java.util.Set; + +/** + * 计量单位服务实现类 + * + * @author Chill + */ +@Service +public class MeasurementUnitServiceImpl extends BaseServiceImpl + implements IMeasurementUnitService { + + private static final int STATUS_ENABLED = 1; + private static final int STATUS_DISABLED = 2; + private static final int UNIT_NAME_MAX_LENGTH = 50; + private static final int DIMENSION_MAX_LENGTH = 20; + private static final int REMARK_MAX_LENGTH = 200; + private static final Set DIMENSIONS = Set.of("重量", "体积", "数量"); + + @Override + public IPage selectMeasurementUnitPage(IPage page, + MeasurementUnitVO measurementUnit) { + return page.setRecords(baseMapper.selectMeasurementUnitPage(page, measurementUnit)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submit(MeasurementUnit measurementUnit) { + prepare(measurementUnit); + validate(measurementUnit); + return saveOrUpdate(measurementUnit); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean changeStatus(Long id, Integer status) { + if (Func.isEmpty(id)) { + throw new ServiceException("主键不能为空"); + } + MeasurementUnit measurementUnit = getById(id); + if (Func.isEmpty(measurementUnit)) { + throw new ServiceException("计量单位不存在"); + } + if (!Objects.equals(status, STATUS_ENABLED) && !Objects.equals(status, STATUS_DISABLED)) { + throw new ServiceException("启停状态不正确"); + } + MeasurementUnit update = new MeasurementUnit(); + update.setId(id); + update.setStatus(status); + return updateById(update); + } + + private void prepare(MeasurementUnit measurementUnit) { + measurementUnit.setUnitName(trimToEmpty(measurementUnit.getUnitName())); + measurementUnit.setDimension(trimToEmpty(measurementUnit.getDimension())); + measurementUnit.setRemark(trimToNull(measurementUnit.getRemark())); + if (Func.isEmpty(measurementUnit.getStatus())) { + measurementUnit.setStatus(STATUS_ENABLED); + } + } + + private void validate(MeasurementUnit measurementUnit) { + if (Func.isEmpty(measurementUnit.getUnitName())) { + throw new ServiceException("计量单位不能为空"); + } + if (measurementUnit.getUnitName().length() > UNIT_NAME_MAX_LENGTH) { + throw new ServiceException("计量单位不能超过50字"); + } + if (Func.isEmpty(measurementUnit.getDimension())) { + throw new ServiceException("计量维度不能为空"); + } + if (measurementUnit.getDimension().length() > DIMENSION_MAX_LENGTH + || !DIMENSIONS.contains(measurementUnit.getDimension())) { + throw new ServiceException("计量维度不正确"); + } + if (Func.isNotEmpty(measurementUnit.getRemark()) + && measurementUnit.getRemark().length() > REMARK_MAX_LENGTH) { + throw new ServiceException("备注不能超过200个字"); + } + if (!Objects.equals(measurementUnit.getStatus(), STATUS_ENABLED) + && !Objects.equals(measurementUnit.getStatus(), STATUS_DISABLED)) { + throw new ServiceException("启停状态不正确"); + } + validateUniqueUnitName(measurementUnit); + } + + private void validateUniqueUnitName(MeasurementUnit measurementUnit) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(MeasurementUnit::getUnitName, measurementUnit.getUnitName()) + .eq(MeasurementUnit::getIsDeleted, 0); + if (Func.isNotEmpty(measurementUnit.getId())) { + queryWrapper.ne(MeasurementUnit::getId, measurementUnit.getId()); + } + if (count(queryWrapper) > 0L) { + throw new ServiceException("该计量单位已存在"); + } + } + + private String trimToEmpty(String value) { + return value == null ? "" : value.trim(); + } + + private String trimToNull(String value) { + String trimValue = trimToEmpty(value); + return trimValue.isEmpty() ? null : trimValue; + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MeasurementUnitWrapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MeasurementUnitWrapper.java new file mode 100644 index 0000000..55f54ea --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MeasurementUnitWrapper.java @@ -0,0 +1,51 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.system.pojo.entity.MeasurementUnit; +import org.springblade.system.pojo.vo.MeasurementUnitVO; + +import java.util.Objects; + +/** + * 计量单位包装类 + * + * @author Chill + */ +public class MeasurementUnitWrapper extends BaseEntityWrapper { + + public static MeasurementUnitWrapper build() { + return new MeasurementUnitWrapper(); + } + + @Override + public MeasurementUnitVO entityVO(MeasurementUnit measurementUnit) { + MeasurementUnitVO measurementUnitVO = Objects.requireNonNull( + BeanUtil.copyProperties(measurementUnit, MeasurementUnitVO.class) + ); + measurementUnitVO.setCreateUserName(UserCache.getUserRealName(measurementUnit.getCreateUser())); + measurementUnitVO.setUpdateUserName(UserCache.getUserRealName(measurementUnit.getUpdateUser())); + return measurementUnitVO; + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VehicleDispatchController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VehicleDispatchController.java new file mode 100644 index 0000000..1eeb004 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VehicleDispatchController.java @@ -0,0 +1,105 @@ +/** + * 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. + *

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

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.excel.util.ExcelUtil; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.excel.VehicleDispatchExcel; +import org.springblade.transport.pojo.entity.VehicleDispatch; +import org.springblade.transport.pojo.vo.VehicleDispatchVO; +import org.springblade.transport.service.IVehicleDispatchService; +import org.springblade.transport.wrapper.VehicleDispatchWrapper; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** 车辆调度申请控制器。 */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "vehicle_dispatch") +@RequestMapping("/vehicle-dispatch") +@Tag(name = "车辆调度", description = "车辆调度申请") +public class VehicleDispatchController extends BladeController { + + private final IVehicleDispatchService vehicleDispatchService; + + @GetMapping("/detail") + @ApiOperationSupport(order = 1) + @Operation(summary = "详情") + public R detail(@Parameter(description = "主键", required = true) @RequestParam Long id) { + VehicleDispatch entity = vehicleDispatchService.getById(id); + if (entity == null) return R.fail("记录不存在"); + return R.data(VehicleDispatchWrapper.build().entityVO(entity)); + } + + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页") + public R> list(VehicleDispatchVO dispatch, Query query) { + return R.data(vehicleDispatchService.selectVehicleDispatchPage(Condition.getPage(query), dispatch)); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 3) + @Operation(summary = "新增或修改") + public R submit(@RequestBody VehicleDispatch dispatch) { + return R.status(vehicleDispatchService.submit(dispatch)); + } + + @PostMapping("/submit-approval") + @ApiOperationSupport(order = 4) + @Operation(summary = "提交审批") + public R submitApproval(@RequestParam Long id) { + return R.status(vehicleDispatchService.submitApproval(id)); + } + + @PostMapping("/approve") + @ApiOperationSupport(order = 5) + @Operation(summary = "审批通过") + public R approve(@RequestParam Long id) { + return R.status(vehicleDispatchService.approve(id)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 6) + @Operation(summary = "逻辑删除") + public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { + return R.status(vehicleDispatchService.deleteLogic(Func.toLongList(ids))); + } + + @GetMapping("/export-vehicle-dispatch") + @ApiOperationSupport(order = 7) + @Operation(summary = "导出车辆调度") + public void export(VehicleDispatchVO dispatch, HttpServletResponse response) { + List list = vehicleDispatchService.exportList(dispatch).stream().map(VehicleDispatchExcel::from).toList(); + ExcelUtil.export(response, "车辆调度" + DateUtil.time(), "车辆调度", list, VehicleDispatchExcel.class); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java new file mode 100644 index 0000000..32f4cf6 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java @@ -0,0 +1,52 @@ +/** + * 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. + *

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

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import lombok.Data; +import org.springblade.transport.pojo.vo.VehicleDispatchVO; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** 车辆调度导出模型。 */ +@Data +@ColumnWidth(20) +public class VehicleDispatchExcel implements Serializable { + @Serial private static final long serialVersionUID = 1L; + @ExcelProperty("申请单号") private String applicationNo; + @ExcelProperty("车牌号") private String plateNo; + @ExcelProperty("所属组织") private String organizationName; + @ExcelProperty("使用部门") private String useDepartment; + @ExcelProperty("审批状态") private String approvalStatusName; + @ExcelProperty("当前节点") private String currentNode; + @ExcelProperty("当前处理人") private String currentProcessor; + @ExcelProperty("创建人") private String createUserName; + @ExcelProperty("创建时间") private Date createTime; + + public static VehicleDispatchExcel from(VehicleDispatchVO source) { + VehicleDispatchExcel target = new VehicleDispatchExcel(); + target.applicationNo = source.getApplicationNo(); + target.plateNo = source.getPlateNo(); + target.organizationName = source.getOrganizationName(); + target.useDepartment = source.getUseDepartment(); + target.approvalStatusName = source.getApprovalStatusName(); + target.currentNode = source.getCurrentNode(); + target.currentProcessor = source.getCurrentProcessor(); + target.createUserName = source.getCreateUserName(); + target.createTime = source.getCreateTime(); + return target; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AccidentRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AccidentRecordMapper.xml index 2e134ff..689037a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AccidentRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AccidentRecordMapper.xml @@ -77,10 +77,10 @@ AND accident_date <= #{accidentRecord.accidentAssessmentDateEnd} - + AND create_time >= #{accidentRecord.createTimeStart} - + AND create_time <= #{accidentRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml index d37d5e3..53f87e9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml @@ -69,10 +69,10 @@ AND inspection_assessment_date <= #{annualInspectionRecord.inspectionAssessmentDateEnd} - + AND create_time >= #{annualInspectionRecord.createTimeStart} - + AND create_time <= #{annualInspectionRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CreditScoreQuantificationMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CreditScoreQuantificationMapper.xml index 250cc9a..d3e9952 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CreditScoreQuantificationMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CreditScoreQuantificationMapper.xml @@ -45,10 +45,10 @@ AND csq.status = #{quantification.status} - + AND csq.create_time >= #{quantification.createTimeStart} - + AND csq.create_time <= #{quantification.createTimeEnd} ORDER BY csq.create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml index f0e3a8d..a9f80c3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml @@ -135,10 +135,10 @@ AND dept_name LIKE #{deptNameLike} - + AND create_time >= #{customer.createTimeStart} - + AND create_time <= #{customer.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceRecordMapper.xml index c70f572..ea73968 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/InsuranceRecordMapper.xml @@ -68,10 +68,10 @@ AND insurance_type = #{insuranceRecord.insuranceType} - + AND create_time >= #{insuranceRecord.createTimeStart} - + AND create_time <= #{insuranceRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenancePlanMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenancePlanMapper.xml index 8bd9c49..b67970d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenancePlanMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenancePlanMapper.xml @@ -69,10 +69,10 @@ AND vehicle_no LIKE #{vehicleNoLike} - + AND create_time >= #{maintenancePlan.createTimeStart} - + AND create_time <= #{maintenancePlan.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenanceRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenanceRecordMapper.xml index 91c1744..527af22 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenanceRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MaintenanceRecordMapper.xml @@ -69,10 +69,10 @@ AND vehicle_no LIKE #{vehicleNoLike} - + AND create_time >= #{maintenanceRecord.createTimeStart} - + AND create_time <= #{maintenanceRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MileageRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MileageRecordMapper.xml index 88022ca..8e3cbc7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MileageRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MileageRecordMapper.xml @@ -63,10 +63,10 @@ AND total_mileage <= #{mileageRecord.totalMileageEnd} - + AND create_time >= #{mileageRecord.createTimeStart} - + AND create_time <= #{mileageRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OilElectricRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OilElectricRecordMapper.xml index 0deb9eb..8861274 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OilElectricRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OilElectricRecordMapper.xml @@ -84,10 +84,10 @@ AND transaction_amount <= #{oilElectricRecord.transactionAmountEnd} - + AND create_time >= #{oilElectricRecord.createTimeStart} - + AND create_time <= #{oilElectricRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OtherExpenseRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OtherExpenseRecordMapper.xml index 80d5d3a..3082f2e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OtherExpenseRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/OtherExpenseRecordMapper.xml @@ -64,10 +64,10 @@ AND expense_date <= #{otherExpenseRecord.expenseDateEnd} - + AND create_time >= #{otherExpenseRecord.createTimeStart} - + AND create_time <= #{otherExpenseRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TireReplacementRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TireReplacementRecordMapper.xml index eba5a70..97cb996 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TireReplacementRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TireReplacementRecordMapper.xml @@ -54,10 +54,10 @@ AND vehicle_no LIKE #{vehicleNoLike} - + AND create_time >= #{tireReplacementRecord.createTimeStart} - + AND create_time <= #{tireReplacementRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportChangeRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportChangeRecordMapper.xml index b4dcb0f..f525cae 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportChangeRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportChangeRecordMapper.xml @@ -55,10 +55,10 @@ AND change_content LIKE #{changeContentLike} - + AND create_time >= #{transportChangeRecord.createTimeStart} - + AND create_time <= #{transportChangeRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.java new file mode 100644 index 0000000..47b9dfc --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.java @@ -0,0 +1,31 @@ +/** + * 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. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Param; +import org.springblade.transport.pojo.entity.VehicleDispatch; +import org.springblade.transport.pojo.vo.VehicleDispatchVO; + +import java.util.List; + +/** 车辆调度申请 Mapper。 */ +public interface VehicleDispatchMapper extends BaseMapper { + List selectVehicleDispatchPage(IPage page, @Param("dispatch") VehicleDispatchVO dispatch); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml new file mode 100644 index 0000000..e372641 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ViolationRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ViolationRecordMapper.xml index 7e75e35..5b90c3f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ViolationRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ViolationRecordMapper.xml @@ -74,10 +74,10 @@ AND process_status = #{violationRecord.processStatus} - + AND create_time >= #{violationRecord.createTimeStart} - + AND create_time <= #{violationRecord.createTimeEnd} ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVehicleDispatchService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVehicleDispatchService.java new file mode 100644 index 0000000..b1d45ad --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVehicleDispatchService.java @@ -0,0 +1,29 @@ +/** + * 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. + *

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

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.entity.VehicleDispatch; +import org.springblade.transport.pojo.vo.VehicleDispatchVO; + +import java.util.List; + +/** 车辆调度申请服务。 */ +public interface IVehicleDispatchService extends BaseService { + IPage selectVehicleDispatchPage(IPage page, VehicleDispatchVO dispatch); + boolean submit(VehicleDispatch dispatch); + boolean submitApproval(Long id); + boolean approve(Long id); + List exportList(VehicleDispatchVO dispatch); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VehicleDispatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VehicleDispatchServiceImpl.java new file mode 100644 index 0000000..04f8010 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VehicleDispatchServiceImpl.java @@ -0,0 +1,127 @@ +/** + * 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. + *

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

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.mapper.TransportVehicleMapper; +import org.springblade.transport.mapper.VehicleDispatchMapper; +import org.springblade.transport.pojo.entity.TransportVehicle; +import org.springblade.transport.pojo.entity.VehicleDispatch; +import org.springblade.transport.pojo.vo.VehicleDispatchVO; +import org.springblade.transport.service.IVehicleDispatchService; +import org.springblade.transport.wrapper.VehicleDispatchWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** 车辆调度申请服务实现。 */ +@Service +@RequiredArgsConstructor +public class VehicleDispatchServiceImpl extends BaseServiceImpl implements IVehicleDispatchService { + + private static final DateTimeFormatter NO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); + + private final TransportVehicleMapper transportVehicleMapper; + + @Override + public IPage selectVehicleDispatchPage(IPage page, VehicleDispatchVO dispatch) { + List records = baseMapper.selectVehicleDispatchPage(page, dispatch); + records.replaceAll(record -> VehicleDispatchWrapper.build().entityVO(record)); + return page.setRecords(records); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submit(VehicleDispatch dispatch) { + if (dispatch == null || Func.isEmpty(dispatch.getPlateNo()) || Func.isEmpty(dispatch.getOrganizationName()) || Func.isEmpty(dispatch.getUseDepartment())) { + throw new ServiceException("车牌号、所属组织和使用部门不能为空"); + } + if (dispatch.getRemark() != null && dispatch.getRemark().length() > 200) { + throw new ServiceException("备注不能超过200个字"); + } + dispatch.setPlateNo(dispatch.getPlateNo().trim().toUpperCase()); + dispatch.setOrganizationName(dispatch.getOrganizationName().trim()); + dispatch.setUseDepartment(dispatch.getUseDepartment().trim()); + if (Func.isEmpty(dispatch.getApprovalStatus())) dispatch.setApprovalStatus("draft"); + if (Func.isEmpty(dispatch.getCurrentNode())) dispatch.setCurrentNode("草稿"); + if (Func.isEmpty(dispatch.getApplicationNo())) { + dispatch.setApplicationNo("CD-" + LocalDate.now().format(NO_DATE_FORMAT) + "-" + (System.currentTimeMillis() % 1000000)); + } + return saveOrUpdate(dispatch); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submitApproval(Long id) { + VehicleDispatch dispatch = getById(id); + if (dispatch == null || !"draft".equals(dispatch.getApprovalStatus()) && !"rejected".equals(dispatch.getApprovalStatus())) { + throw new ServiceException("仅草稿或已驳回申请可以提交审批"); + } + VehicleDispatch update = new VehicleDispatch(); + update.setId(id); + update.setApprovalStatus("reviewing"); + update.setCurrentNode("车辆调度审批"); + update.setCurrentProcessor(null); + return updateById(update); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean approve(Long id) { + VehicleDispatch dispatch = getById(id); + if (dispatch == null) { + throw new ServiceException("车辆调度申请不存在"); + } + if (!"reviewing".equals(dispatch.getApprovalStatus())) { + throw new ServiceException("仅审批中的车辆调度申请可以审核通过"); + } + + TransportVehicle vehicle = transportVehicleMapper.selectOne(Wrappers.lambdaQuery() + .eq(TransportVehicle::getIsDeleted, 0) + .eq(TransportVehicle::getPlateNo, dispatch.getPlateNo()) + .last("LIMIT 1")); + if (vehicle == null) { + throw new ServiceException("调度车辆不存在,无法同步使用部门"); + } + + TransportVehicle vehicleUpdate = new TransportVehicle(); + vehicleUpdate.setId(vehicle.getId()); + vehicleUpdate.setUseDepartment(dispatch.getUseDepartment()); + if (transportVehicleMapper.updateById(vehicleUpdate) <= 0) { + throw new ServiceException("车辆使用部门同步失败"); + } + + VehicleDispatch dispatchUpdate = new VehicleDispatch(); + dispatchUpdate.setId(dispatch.getId()); + dispatchUpdate.setApprovalStatus("approved"); + dispatchUpdate.setCurrentNode("审批通过"); + dispatchUpdate.setCurrentProcessor(AuthUtil.getUserName()); + return updateById(dispatchUpdate); + } + + @Override + public List exportList(VehicleDispatchVO dispatch) { + IPage page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(1, 10000); + return selectVehicleDispatchPage(page, dispatch).getRecords(); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/VehicleDispatchWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/VehicleDispatchWrapper.java new file mode 100644 index 0000000..c116de1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/VehicleDispatchWrapper.java @@ -0,0 +1,49 @@ +/** + * 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. + *

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

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.transport.pojo.entity.VehicleDispatch; +import org.springblade.transport.pojo.vo.VehicleDispatchVO; + +import java.util.Objects; + +/** 车辆调度申请包装类。 */ +public class VehicleDispatchWrapper extends BaseEntityWrapper { + + public static VehicleDispatchWrapper build() { + return new VehicleDispatchWrapper(); + } + + @Override + public VehicleDispatchVO entityVO(VehicleDispatch entity) { + if (entity == null) return null; + VehicleDispatchVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, VehicleDispatchVO.class)); + vo.setCreateUserName(org.springblade.system.cache.UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(org.springblade.system.cache.UserCache.getUserRealName(entity.getUpdateUser())); + vo.setApprovalStatusName(statusName(entity.getApprovalStatus())); + return vo; + } + + private String statusName(String status) { + if (status == null) return "未知"; + return switch (status) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "rejected" -> "已驳回"; + case "approved" -> "审批通过"; + default -> "未知"; + }; + } +} diff --git a/doc/sql/bladex/bladex.mysql.all.create.sql b/doc/sql/bladex/bladex.mysql.all.create.sql index 8728449..0542044 100644 --- a/doc/sql/bladex/bladex.mysql.all.create.sql +++ b/doc/sql/bladex/bladex.mysql.all.create.sql @@ -1750,4 +1750,36 @@ INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `s (1980000000000000077, 2075449200000000001, 'fee_item_template', '模板下载', 'template', '/api/blade-system/fee-item/export-template', 'download', 7, 2, 2, 1, '', NULL, 0), (1980000000000000078, 2075449200000000001, 'fee_item_export', '批量导出', 'export', '/api/blade-system/fee-item/export-fee-item', 'download', 8, 2, 2, 1, '', NULL, 0); +-- ---------------------------- +-- Table structure for blade_measurement_unit +-- ---------------------------- +DROP TABLE IF EXISTS `blade_measurement_unit`; +CREATE TABLE `blade_measurement_unit` ( + `id` bigint NOT NULL COMMENT '主键', + `unit_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量单位', + `dimension` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量维度', + `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_user` bigint NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', + `status` int NULL DEFAULT 1 COMMENT '状态', + `is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_measurement_unit_name`(`unit_name`) USING BTREE, + INDEX `idx_measurement_unit_dimension`(`dimension`) USING BTREE, + INDEX `idx_measurement_unit_status`(`status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '计量单位'; + +-- ---------------------------- +-- Records of blade_menu for measurement unit +-- ---------------------------- +INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES +(2075449300000000001, 1164733399668962201, 'measurement_unit', '计量单位', 'menu', '/base/measurement-unit', 'iconfont icon-shoucang', 8, 1, 0, 1, '', NULL, 0), +(2075449300000000002, 2075449300000000001, 'measurement_unit_add', '新增', 'add', '/base/measurement-unit/add', 'plus', 1, 2, 1, 1, '', NULL, 0), +(2075449300000000003, 2075449300000000001, 'measurement_unit_edit', '修改', 'edit', '/base/measurement-unit/edit', 'form', 2, 2, 2, 1, '', NULL, 0), +(2075449300000000004, 2075449300000000001, 'measurement_unit_delete', '删除', 'delete', '/api/blade-system/measurement-unit/remove', 'delete', 3, 2, 3, 1, '', NULL, 0), +(2075449300000000005, 2075449300000000001, 'measurement_unit_status', '启停', 'status', '/api/blade-system/measurement-unit/status', 'key', 4, 2, 2, 1, '', NULL, 0); + SET FOREIGN_KEY_CHECKS = 1; diff --git a/doc/sql/transport/blade_measurement_unit.sql b/doc/sql/transport/blade_measurement_unit.sql new file mode 100644 index 0000000..8f03d43 --- /dev/null +++ b/doc/sql/transport/blade_measurement_unit.sql @@ -0,0 +1,31 @@ +-- ---------------------------- +-- Table structure for blade_measurement_unit +-- ---------------------------- +DROP TABLE IF EXISTS `blade_measurement_unit`; +CREATE TABLE `blade_measurement_unit` ( + `id` bigint NOT NULL COMMENT '主键', + `unit_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量单位', + `dimension` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量维度', + `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_user` bigint NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', + `status` int NULL DEFAULT 1 COMMENT '状态', + `is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_measurement_unit_name`(`unit_name`) USING BTREE, + INDEX `idx_measurement_unit_dimension`(`dimension`) USING BTREE, + INDEX `idx_measurement_unit_status`(`status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '计量单位'; + +-- ---------------------------- +-- Records of blade_menu for measurement unit +-- ---------------------------- +INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES +(2075449300000000001, 1164733399668962201, 'measurement_unit', '计量单位', 'measurement_unit', '/base/measurement-unit', 'iconfont icon-shoucang', 80, 1, 0, 1, NULL, '', 0), +(2075449300000000002, 2075449300000000001, 'measurement_unit_delete', '删除', 'measurement_unit_delete', '', '', 1, 2, 0, 1, NULL, '', 0), +(2075449300000000003, 2075449300000000001, 'measurement_unit_edit', '编辑', 'measurement_unit_edit', '', '', 1, 2, 0, 1, NULL, '', 0), +(2075449300000000004, 2075449300000000001, 'measurement_unit_status', '修改状态', 'measurement_unit_status', '', '', 1, 2, 0, 1, NULL, '', 0), +(2075449300000000005, 2075449300000000001, 'measurement_unit_add', '新增', 'measurement_unit_add', '', '', 1, 2, 0, 1, NULL, '', 0); diff --git a/doc/sql/transport/blade_vehicle_dispatch.sql b/doc/sql/transport/blade_vehicle_dispatch.sql new file mode 100644 index 0000000..e8fc0b8 --- /dev/null +++ b/doc/sql/transport/blade_vehicle_dispatch.sql @@ -0,0 +1,44 @@ +-- ---------------------------- +-- Table structure for blade_transport_vehicle_dispatch +-- ---------------------------- +DROP TABLE IF EXISTS `blade_transport_vehicle_dispatch`; +CREATE TABLE `blade_transport_vehicle_dispatch` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户ID', + `application_no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '申请单号', + `plate_no` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '车牌号', + `organization_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所属组织', + `use_department` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '使用部门', + `approval_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'draft' COMMENT '审批状态:draft草稿/reviewing审批中/rejected已驳回/approved审批通过', + `current_node` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前节点', + `current_processor` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前处理人', + `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `attachments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '附件JSON', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `status` int(2) DEFAULT 1 COMMENT '状态', + `is_deleted` int(2) DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_vehicle_dispatch_application_no` (`application_no`), + KEY `idx_vehicle_dispatch_plate_no` (`plate_no`), + KEY `idx_vehicle_dispatch_approval_status` (`approval_status`), + KEY `idx_vehicle_dispatch_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='车辆调度申请'; + +-- ---------------------------- +-- Menu records for transport capacity +-- ---------------------------- +INSERT INTO `blade_menu` +(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) +VALUES +(2079000000000000100, 2079000000000000001, 'vehicle_dispatch', '车辆调度', 'vehicle_dispatch', '/transportCapacity/vehicle-dispatch', 'iconfont icon-yunshu', 3, 1, 0, 1, '', '', 0), +(2079000000000000101, 2079000000000000100, 'vehicle_dispatch_add', '新增', 'vehicle_dispatch_add', '', '', 1, 2, 0, 1, NULL, '', 0), +(2079000000000000102, 2079000000000000100, 'vehicle_dispatch_edit', '编辑', 'vehicle_dispatch_edit', '', '', 2, 2, 0, 1, NULL, '', 0), +(2079000000000000103, 2079000000000000100, 'vehicle_dispatch_delete', '删除', 'vehicle_dispatch_delete', '', '', 3, 2, 0, 1, NULL, '', 0), +(2079000000000000104, 2079000000000000100, 'vehicle_dispatch_view', '查看', 'vehicle_dispatch_view', '', '', 4, 2, 0, 1, NULL, '', 0), +(2079000000000000105, 2079000000000000100, 'vehicle_dispatch_submit', '提交审批', 'vehicle_dispatch_submit', '', '', 5, 2, 0, 1, NULL, '', 0), +(2079000000000000106, 2079000000000000100, 'vehicle_dispatch_export', '导出', 'vehicle_dispatch_export', '', '', 6, 2, 0, 1, NULL, '', 0), +(2079000000000000107, 2079000000000000100, 'vehicle_dispatch_approve', '审核通过', 'vehicle_dispatch_approve', '', '', 7, 2, 0, 1, NULL, '', 0); diff --git a/hs_err_pid25098.log b/hs_err_pid25098.log new file mode 100644 index 0000000..c5bda44 --- /dev/null +++ b/hs_err_pid25098.log @@ -0,0 +1,1375 @@ +# +# A fatal error has been detected by the Java Runtime Environment: +# +# SIGBUS (0xa) at pc=0x0000000104a124c0, pid=25098, tid=36379 +# +# JRE version: OpenJDK Runtime Environment Zulu17.44+15-CA (17.0.8+7) (build 17.0.8+7-LTS) +# Java VM: OpenJDK 64-Bit Server VM Zulu17.44+15-CA (17.0.8+7-LTS, mixed mode, emulated-client, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64) +# Problematic frame: +# C [libzip.dylib+0x64c0] newEntry+0x68 +# +# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again +# +# If you would like to submit a bug report, please visit: +# http://www.azul.com/support/ +# The crash happened outside the Java Virtual Machine in native code. +# See problematic frame for where to report the bug. +# + +--------------- S U M M A R Y ------------ + +Command Line: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:57772,suspend=y,server=n -javaagent:/Users/liangxin/Library/Caches/JetBrains/IntelliJIdea2026.1/captureAgent/debugger-agent.jar=file:///var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/capture2936228657643338079.props -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Ddebugger.async.stack.trace.for.all.threads=true -Dfile.encoding=UTF-8 org.springblade.transport.TransportApplication + +Host: "Mac15,6" arm64, 12 cores, 36G, Darwin 25.6.0, macOS 26.6.2 (25G83) +Time: Mon Sep 14 01:37:22 2026 CST elapsed time: 8.929714 seconds (0d 0h 0m 8s) + +--------------- T H R E A D --------------- + +Current thread (0x00000001178ff000): JavaThread "sentinel-datafile-log-executor-thread-1" daemon [_thread_in_native, id=36379, stack(0x000000031147c000,0x000000031167f000)] + +Stack: [0x000000031147c000,0x000000031167f000], sp=0x000000031167d9d0, free space=2054k +Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) +C [libzip.dylib+0x64c0] newEntry+0x68 +C [libzip.dylib+0x6390] ZIP_GetEntry2+0x14c +C [libzip.dylib+0x6d78] ZIP_FindEntry+0x3c +V [libjvm.dylib+0x25a408] ClassPathZipEntry::open_entry(JavaThread*, char const*, int*, bool)+0xb4 +V [libjvm.dylib+0x25a53c] ClassPathZipEntry::open_stream(JavaThread*, char const*)+0x20 +V [libjvm.dylib+0x25d918] ClassLoader::load_class(Symbol*, bool, JavaThread*)+0x150 +V [libjvm.dylib+0x981d60] SystemDictionary::load_instance_class_impl(Symbol*, Handle, JavaThread*)+0x2d0 +V [libjvm.dylib+0x98063c] SystemDictionary::load_instance_class(unsigned int, Symbol*, Handle, JavaThread*)+0x30 +V [libjvm.dylib+0x97fd48] SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, JavaThread*)+0x4dc +V [libjvm.dylib+0x97f334] SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, JavaThread*)+0x80 +V [libjvm.dylib+0x2beb54] ConstantPool::klass_at_impl(constantPoolHandle const&, int, JavaThread*)+0x1e0 +V [libjvm.dylib+0x46d6f0] InterpreterRuntime::_new(JavaThread*, ConstantPool*, int)+0x94 +j com.intellij.rt.debugger.agent.CaptureStorage$DeepCapturedStack.collectStacks(Ljava/util/List;)Lcom/intellij/rt/debugger/agent/CaptureStorage$StackData;+89 +j com.intellij.rt.debugger.agent.CaptureStorage.getStackTrace(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+30 +j com.intellij.rt.debugger.agent.CaptureStorage.access$1000(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()[Ljava/lang/StackTraceElement;+31 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()Ljava/lang/Object;+1 +j com.intellij.rt.debugger.agent.CaptureStorage.withoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$Callable;)Ljava/lang/Object;+21 +j com.intellij.rt.debugger.agent.CaptureStorage.getAsyncStackTrace(Ljava/lang/Throwable;)[Ljava/lang/StackTraceElement;+19 +j java.lang.Throwable.printStackTrace(Ljava/lang/Throwable$PrintStreamOrWriter;)V+32 java.base@17.0.8 +j java.lang.Throwable.printStackTrace(Ljava/io/PrintWriter;)V+9 java.base@17.0.8 +j com.alibaba.csp.sentinel.log.jul.CspFormatter.format(Ljava/util/logging/LogRecord;)Ljava/lang/String;+103 +j java.util.logging.StreamHandler.publish(Ljava/util/logging/LogRecord;)V+14 java.logging@17.0.8 +j java.util.logging.FileHandler.publish(Ljava/util/logging/LogRecord;)V+11 java.logging@17.0.8 +j com.alibaba.csp.sentinel.log.jul.DateFileLogHandler$LogTask.run()V+8 +j java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+92 java.base@17.0.8 +j java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5 java.base@17.0.8 +j java.lang.Thread.run()V+11 java.base@17.0.8 +v ~StubRoutines::call_stub +V [libjvm.dylib+0x4781f8] JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)+0x394 +V [libjvm.dylib+0x47720c] JavaCalls::call_virtual(JavaValue*, Klass*, Symbol*, Symbol*, JavaCallArguments*, JavaThread*)+0x11c +V [libjvm.dylib+0x4772d8] JavaCalls::call_virtual(JavaValue*, Handle, Klass*, Symbol*, Symbol*, JavaThread*)+0x64 +V [libjvm.dylib+0x52ebfc] thread_entry(JavaThread*, JavaThread*)+0xc4 +V [libjvm.dylib+0x9b22e8] JavaThread::thread_main_inner()+0x150 +V [libjvm.dylib+0x9b0990] Thread::call_run()+0xe0 +V [libjvm.dylib+0x7d0364] thread_native_entry(Thread*)+0x158 +C [libsystem_pthread.dylib+0x6c58] _pthread_start+0x88 + +Java frames: (J=compiled Java code, j=interpreted, Vv=VM code) +j com.intellij.rt.debugger.agent.CaptureStorage$DeepCapturedStack.collectStacks(Ljava/util/List;)Lcom/intellij/rt/debugger/agent/CaptureStorage$StackData;+89 +j com.intellij.rt.debugger.agent.CaptureStorage.getStackTrace(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+30 +j com.intellij.rt.debugger.agent.CaptureStorage.access$1000(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()[Ljava/lang/StackTraceElement;+31 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()Ljava/lang/Object;+1 +j com.intellij.rt.debugger.agent.CaptureStorage.withoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$Callable;)Ljava/lang/Object;+21 +j com.intellij.rt.debugger.agent.CaptureStorage.getAsyncStackTrace(Ljava/lang/Throwable;)[Ljava/lang/StackTraceElement;+19 +j java.lang.Throwable.printStackTrace(Ljava/lang/Throwable$PrintStreamOrWriter;)V+32 java.base@17.0.8 +j java.lang.Throwable.printStackTrace(Ljava/io/PrintWriter;)V+9 java.base@17.0.8 +j com.alibaba.csp.sentinel.log.jul.CspFormatter.format(Ljava/util/logging/LogRecord;)Ljava/lang/String;+103 +j java.util.logging.StreamHandler.publish(Ljava/util/logging/LogRecord;)V+14 java.logging@17.0.8 +j java.util.logging.FileHandler.publish(Ljava/util/logging/LogRecord;)V+11 java.logging@17.0.8 +j com.alibaba.csp.sentinel.log.jul.DateFileLogHandler$LogTask.run()V+8 +j java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+92 java.base@17.0.8 +j java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5 java.base@17.0.8 +j java.lang.Thread.run()V+11 java.base@17.0.8 +v ~StubRoutines::call_stub + +siginfo: si_signo: 10 (SIGBUS), si_code: 1 (BUS_ADRALN), si_addr: 0x000000010490de7b + +Register to memory mapping: + + x0=0x0000600001c38c80 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x1=0x0 is NULL + x2=0xffffffffffffffd0 is an unknown value + x3=0x0000600001c38c90 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x4=0x0000600001c38d00 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x5=0x000000008cc51ffb is an unknown value + x6=0x000000000ce00000 is an unknown value + x7=0x000000000000000a is an unknown value + x8=0x0000000104a35e5f points into unknown readable memory: 0a + x9=0x0000000000128000 is an unknown value +x10=0x0000600001c38000 points into unknown readable memory: 0x726f0040f29d0001 | 01 00 9d f2 40 00 6f 72 +x11=0x0000000000000c80 is an unknown value +x12=0x0000000000000050 is an unknown value +x13=0x0000000000000001 is an unknown value +x14=0x00000000ffffff5c is an unknown value +x15=0x00000000000007fb is an unknown value +x16=0x0000000185ab9030: __bzero+0 in /usr/lib/system/libsystem_platform.dylib at 0x0000000185ab6000 +x17=0x00000001f3b314a8 points into unknown readable memory: 0x0000000185ab9030 | 30 90 ab 85 01 00 00 00 +x18=0x0 is NULL +x19=0x0000600001c38c80 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x20=0x0 is NULL +x21=0x00006000009a8000 points into unknown readable memory: 0x00006000018b0180 | 80 01 8b 01 00 60 00 00 +x22=0x000000010490de5f points into unknown readable memory: 50 +x23=0x00000000d3a18b02 is an unknown value +x24=0x000000000000002f is an unknown value +x25=0x000000000000003d is an unknown value +x26=0x00000000000000cd is an unknown value +x27=0x0000600001c38ca8 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x28=0x0000000146e163d0 points into unknown readable memory: 0x65746e692f6d6f63 | 63 6f 6d 2f 69 6e 74 65 + + +Registers: + x0=0x0000600001c38c80 x1=0x0000000000000000 x2=0xffffffffffffffd0 x3=0x0000600001c38c90 + x4=0x0000600001c38d00 x5=0x000000008cc51ffb x6=0x000000000ce00000 x7=0x000000000000000a + x8=0x0000000104a35e5f x9=0x0000000000128000 x10=0x0000600001c38000 x11=0x0000000000000c80 +x12=0x0000000000000050 x13=0x0000000000000001 x14=0x00000000ffffff5c x15=0x00000000000007fb +x16=0x0000000185ab9030 x17=0x00000001f3b314a8 x18=0x0000000000000000 x19=0x0000600001c38c80 +x20=0x0000000000000000 x21=0x00006000009a8000 x22=0x000000010490de5f x23=0x00000000d3a18b02 +x24=0x000000000000002f x25=0x000000000000003d x26=0x00000000000000cd x27=0x0000600001c38ca8 +x28=0x0000000146e163d0 fp=0x000000031167da50 lr=0x0000000104a1248c sp=0x000000031167d9d0 +pc=0x0000000104a124c0 cpsr=0x0000000080001000 +Top of Stack: (sp=0x000000031167d9d0) +0x000000031167d9d0: 0000000000000000 0000000000000000 +0x000000031167d9e0: 0000000000000000 0000000000000000 +0x000000031167d9f0: 0000000000000000 0000000000000000 +0x000000031167da00: 0000000146e163d0 0000000159809800 +0x000000031167da10: 00000000000000cd 000000000000003d +0x000000031167da20: 000000000000002f 00000000d3a18b02 +0x000000031167da30: 0000600001c2e120 0000000000000000 +0x000000031167da40: 0000000146e16410 00006000009a8000 +0x000000031167da50: 000000031167dab0 0000000104a12390 +0x000000031167da60: 0000000146e163d0 0000000000000037 +0x000000031167da70: 0000000000000001 0000000146e16410 +0x000000031167da80: 00000001178ff348 0000000146e16410 +0x000000031167da90: 00006000009a8000 0000000146e16410 +0x000000031167daa0: 000000031167dbdc 000000031167daf4 +0x000000031167dab0: 000000031167dae0 0000000104a12d78 +0x000000031167dac0: 000000031167dbdc 00006000032b4150 +0x000000031167dad0: 0000000000000000 00000001178ff000 +0x000000031167dae0: 000000031167dbc0 0000000105d96408 +0x000000031167daf0: 00000001065f4388 0000000000000100 +0x000000031167db00: 000000031167db20 000000010606a5dc +0x000000031167db10: 00000001065f4388 000000031167dba0 +0x000000031167db20: 000000031167db70 0000000105e1c96c +0x000000031167db30: 0000000000000000 0000000000000000 +0x000000031167db40: 0000000000000001 00000001280a0290 +0x000000031167db50: 00000001178ff000 0000000146e167a8 +0x000000031167db60: 00000001066091e2 000000031167dc68 +0x000000031167db70: 000000031167db90 dde20fe0350d0029 +0x000000031167db80: 0000000000000001 0000000146e16410 +0x000000031167db90: 00006000032b4150 00000001280a0290 +0x000000031167dba0: 00000001178ff000 0000000146e167a8 +0x000000031167dbb0: 0000000146e163c0 00006000032b4150 +0x000000031167dbc0: 000000031167dbf0 0000000105d9653c + +Instructions: (pc=0x0000000104a124c0) +0x0000000104a123c0: 6b0c017f 54ffff60 17ffffde d2800016 +0x0000000104a123d0: 72001ebf 54000160 b5000156 f100073f +0x0000000104a123e0: 54fff7cb 8b140328 385ff108 7100bd1f +0x0000000104a123f0: 54fff741 d2800016 14000002 f9004e7f +0x0000000104a12400: f9402a60 94000451 aa1603e0 a9457bfd +0x0000000104a12410: a9444ff4 a94357f6 a9425ff8 a94167fa +0x0000000104a12420: a8c66ffc d65f03c0 6b03003f 540000e1 +0x0000000104a12430: 71000421 540000eb 38401408 38401449 +0x0000000104a12440: 6b09011f 54ffff60 52800000 d65f03c0 +0x0000000104a12450: 52800020 d65f03c0 d10243ff a9036ffc +0x0000000104a12460: a90467fa a9055ff8 a90657f6 a9074ff4 +0x0000000104a12470: a9087bfd 910203fd aa0203f4 aa0103f6 +0x0000000104a12480: aa0003f5 52800900 94000481 aa0003f3 +0x0000000104a12490: b4001320 f900027f aa1303fb f8028f7f +0x0000000104a124a0: f9001a7f 3940c2a8 34000288 f9400ea8 +0x0000000104a124b0: f94006c9 8b090108 f94016a9 cb090116 +0x0000000104a124c0: 79403ad8 39407ada 39407edc 794042c8 +0x0000000104a124d0: f90017e8 b9400ec8 f9000668 b9401ac8 +0x0000000104a124e0: f9000fe8 f9000a68 794016c8 34000488 +0x0000000104a124f0: b94016c8 14000023 f94006d7 34000d54 +0x0000000104a12500: f9401ea8 b4000288 f94022a9 eb17013f +0x0000000104a12510: 5400022c 5283fa4a 8b0a012a eb17015f +0x0000000104a12520: 540001ab 9140092a 8b170108 cb090116 +0x0000000104a12530: 79403ac8 79403ec9 794042cb 8b0802e8 +0x0000000104a12540: 8b090108 8b0b0108 9100b908 eb0a011f +0x0000000104a12550: 54000b4d aa1503e0 aa1703e1 52840002 +0x0000000104a12560: 94000384 aa0003f6 b4000aa0 f9401ea0 +0x0000000104a12570: 94000429 a903deb6 17ffffd2 d2800008 +0x0000000104a12580: aa0803f7 f9000e68 b94012c8 b9002268 +0x0000000104a12590: b842a2c9 f9405ea8 f9000be9 8b090108 +0x0000000104a125a0: cb0803e8 f9001e68 794012c8 b9004268 +0x0000000104a125b0: 91000700 94000436 aa0003f9 f9000260 + + +Stack slot to memory mapping: +stack at sp + 0 slots: 0x0 is NULL +stack at sp + 1 slots: 0x0 is NULL +stack at sp + 2 slots: 0x0 is NULL +stack at sp + 3 slots: 0x0 is NULL +stack at sp + 4 slots: 0x0 is NULL +stack at sp + 5 slots: 0x0 is NULL +stack at sp + 6 slots: 0x0000000146e163d0 points into unknown readable memory: 0x65746e692f6d6f63 | 63 6f 6d 2f 69 6e 74 65 +stack at sp + 7 slots: 0x0000000159809800 points into unknown readable memory: 0xffffffff5bbd78a2 | a2 78 bd 5b ff ff ff ff + + +--------------- P R O C E S S --------------- + +Threads class SMR info: +_java_thread_list=0x0000600003a5aec0, length=73, elements={ +0x0000000127008600, 0x000000012701a800, 0x0000000159809200, 0x0000000117008200, +0x000000011700ba00, 0x000000012701da00, 0x000000012701e000, 0x0000000117014000, +0x0000000105010a00, 0x000000015701ba00, 0x0000000137023800, 0x000000013701c200, +0x0000000105008200, 0x00000001278bcc00, 0x0000000157011e00, 0x0000000127900000, +0x0000000137023e00, 0x0000000117820600, 0x0000000117440400, 0x0000000127b6c800, +0x000000011752ae00, 0x00000001590ed600, 0x000000010542d200, 0x0000000117582c00, +0x00000001473b0800, 0x0000000157442800, 0x0000000157469a00, 0x0000000158186a00, +0x0000000127070600, 0x0000000127c83600, 0x00000001588e8600, 0x0000000117895e00, +0x000000015910e000, 0x00000001176b1800, 0x00000001054d9000, 0x0000000127cff600, +0x00000001176d7800, 0x000000011768a200, 0x00000001176f7c00, 0x0000000159134a00, +0x00000001178c8a00, 0x00000001574f1600, 0x0000000157557000, 0x00000001588c5a00, +0x0000000105539e00, 0x0000000117724c00, 0x000000015890a200, 0x00000001270c2a00, +0x0000000158924a00, 0x00000001598d1600, 0x0000000127d62e00, 0x00000001581bee00, +0x0000000117718c00, 0x0000000147538000, 0x0000000127da4a00, 0x0000000137555e00, +0x0000000147537a00, 0x0000000117731c00, 0x00000001374c1800, 0x0000000159930000, +0x00000001178ff000, 0x00000001055f5200, 0x00000001055e8200, 0x0000000147613600, +0x0000000157615000, 0x000000015762ea00, 0x0000000127e76400, 0x0000000127171400, +0x000000015765fe00, 0x0000000159961c00, 0x00000001300bb200, 0x000000010576e600, +0x000000012717f600 +} + +Java Threads: ( => current thread ) + 0x0000000127008600 JavaThread "main" [_thread_in_native, id=4099, stack(0x000000016b640000,0x000000016b843000)] + 0x000000012701a800 JavaThread "Reference Handler" daemon [_thread_blocked, id=20483, stack(0x0000000175494000,0x0000000175697000)] + 0x0000000159809200 JavaThread "Finalizer" daemon [_thread_blocked, id=20227, stack(0x00000001756a0000,0x00000001758a3000)] + 0x0000000117008200 JavaThread "Signal Dispatcher" daemon [_thread_blocked, id=30467, stack(0x00000001759c4000,0x0000000175bc7000)] + 0x000000011700ba00 JavaThread "Service Thread" daemon [_thread_blocked, id=30211, stack(0x0000000175bd0000,0x0000000175dd3000)] + 0x000000012701da00 JavaThread "Monitor Deflation Thread" daemon [_thread_blocked, id=29955, stack(0x0000000175ddc000,0x0000000175fdf000)] + 0x000000012701e000 JavaThread "C1 CompilerThread0" daemon [_thread_blocked, id=23043, stack(0x0000000175fe8000,0x00000001761eb000)] + 0x0000000117014000 JavaThread "Sweeper thread" daemon [_thread_blocked, id=29187, stack(0x00000001761f4000,0x00000001763f7000)] + 0x0000000105010a00 JavaThread "C1 CompilerThread1" daemon [_thread_blocked, id=23555, stack(0x0000000176400000,0x0000000176603000)] + 0x000000015701ba00 JavaThread "Common-Cleaner" daemon [_thread_blocked, id=23811, stack(0x000000017660c000,0x000000017680f000)] + 0x0000000137023800 JavaThread "JDWP Transport Listener: dt_socket" daemon [_thread_blocked, id=24323, stack(0x0000000176818000,0x0000000176a1b000)] + 0x000000013701c200 JavaThread "JDWP Event Helper Thread" daemon [_thread_blocked, id=28675, stack(0x0000000176a24000,0x0000000176c27000)] + 0x0000000105008200 JavaThread "JDWP Command Reader" daemon [_thread_in_native, id=28419, stack(0x0000000176c30000,0x0000000176e33000)] + 0x00000001278bcc00 JavaThread "IntelliJ Suspend Helper" daemon [_thread_blocked, id=27907, stack(0x0000000176e3c000,0x000000017703f000)] + 0x0000000157011e00 JavaThread "Notification Thread" daemon [_thread_blocked, id=25091, stack(0x0000000177048000,0x000000017724b000)] + 0x0000000127900000 JavaThread "CoarseTimer" daemon [_thread_blocked, id=25347, stack(0x0000000177254000,0x0000000177457000)] + 0x0000000137023e00 JavaThread "C1 CompilerThread2" daemon [_thread_blocked, id=25603, stack(0x0000000177460000,0x0000000177663000)] + 0x0000000117820600 JavaThread "C1 CompilerThread3" daemon [_thread_blocked, id=25859, stack(0x000000017766c000,0x000000017786f000)] + 0x0000000117440400 JavaThread "RMI TCP Accept-0" daemon [_thread_in_native, id=35587, stack(0x0000000310c4c000,0x0000000310e4f000)] + 0x0000000127b6c800 JavaThread "com.alibaba.nacos.client.logging.0" daemon [_thread_blocked, id=35843, stack(0x0000000311064000,0x0000000311267000)] + 0x000000011752ae00 JavaThread "Attach Listener" daemon [_thread_blocked, id=40195, stack(0x0000000311cac000,0x0000000311eaf000)] + 0x00000001590ed600 JavaThread "nacos.publisher-com.alibaba.nacos.common.notify.SlowEvent" daemon [_thread_blocked, id=38915, stack(0x00000003122d0000,0x00000003124d3000)] + 0x000000010542d200 JavaThread "nacos.publisher-com.alibaba.nacos.client.config.impl.ConfigFuzzyWatchNotifyEvent" daemon [_thread_blocked, id=39683, stack(0x00000003124dc000,0x00000003126df000)] + 0x0000000117582c00 JavaThread "nacos.publisher-com.alibaba.nacos.client.config.impl.ConfigFuzzyWatchLoadEvent" daemon [_thread_blocked, id=43523, stack(0x00000003126e8000,0x00000003128eb000)] + 0x00000001473b0800 JavaThread "RMI TCP Connection(idle)" daemon [_thread_blocked, id=65283, stack(0x00000003128f4000,0x0000000312af7000)] + 0x0000000157442800 JavaThread "RMI Scheduler(0)" daemon [_thread_blocked, id=65027, stack(0x0000000312b00000,0x0000000312d03000)] + 0x0000000157469a00 JavaThread "com.alibaba.nacos.client.auth.ram.identify.watcher.0" daemon [_thread_blocked, id=44559, stack(0x0000000312d0c000,0x0000000312f0f000)] + 0x0000000158186a00 JavaThread "com.alibaba.nacos.client.login-executor.0" daemon [_thread_blocked, id=37647, stack(0x0000000311aa0000,0x0000000311ca3000)] + 0x0000000127070600 JavaThread "com.alibaba.nacos.client.listen-executor.0" daemon [_thread_blocked, id=64771, stack(0x0000000312f18000,0x000000031311b000)] + 0x0000000127c83600 JavaThread "com.alibaba.nacos.client.fuzzy-watcher-executor.0" daemon [_thread_blocked, id=64259, stack(0x0000000313124000,0x0000000313327000)] + 0x00000001588e8600 JavaThread "com.alibaba.nacos.client.remote.worker.0" daemon [_thread_blocked, id=63747, stack(0x0000000313330000,0x0000000313533000)] + 0x0000000117895e00 JavaThread "com.alibaba.nacos.client.remote.worker.1" daemon [_thread_blocked, id=45059, stack(0x000000031353c000,0x000000031373f000)] + 0x000000015910e000 JavaThread "grpc-nio-worker-ELG-1-1" daemon [_thread_in_native, id=63247, stack(0x0000000313748000,0x000000031394b000)] + 0x00000001176b1800 JavaThread "grpc-default-executor-0" daemon [_thread_blocked, id=45571, stack(0x0000000313954000,0x0000000313b57000)] + 0x00000001054d9000 JavaThread "nacos-grpc-client-executor-127.0.0.1-0" daemon [_thread_blocked, id=46083, stack(0x0000000313b60000,0x0000000313d63000)] + 0x0000000127cff600 JavaThread "nacos-grpc-client-executor-127.0.0.1-1" daemon [_thread_blocked, id=46339, stack(0x0000000313d6c000,0x0000000313f6f000)] + 0x00000001176d7800 JavaThread "RMI TCP Connection(4)-127.0.0.1" daemon [_thread_in_native, id=46595, stack(0x0000000313f78000,0x000000031417b000)] + 0x000000011768a200 JavaThread "grpc-nio-worker-ELG-1-2" daemon [_thread_in_native, id=61967, stack(0x0000000314184000,0x0000000314387000)] + 0x00000001176f7c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-2" daemon [_thread_blocked, id=46851, stack(0x0000000314390000,0x0000000314593000)] + 0x0000000159134a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-3" daemon [_thread_blocked, id=61187, stack(0x000000031459c000,0x000000031479f000)] + 0x00000001178c8a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-4" daemon [_thread_blocked, id=60675, stack(0x00000003147a8000,0x00000003149ab000)] + 0x00000001574f1600 JavaThread "nacos-grpc-client-executor-127.0.0.1-5" daemon [_thread_blocked, id=60163, stack(0x00000003149b4000,0x0000000314bb7000)] + 0x0000000157557000 JavaThread "nacos-grpc-client-executor-127.0.0.1-6" daemon [_thread_blocked, id=47107, stack(0x0000000314bc0000,0x0000000314dc3000)] + 0x00000001588c5a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-7" daemon [_thread_blocked, id=59651, stack(0x0000000314dcc000,0x0000000314fcf000)] + 0x0000000105539e00 JavaThread "nacos.publisher-com.alibaba.nacos.common.ability.AbstractAbilityControlManager$AbilityUpdateEvent" daemon [_thread_blocked, id=59139, stack(0x0000000314fd8000,0x00000003151db000)] + 0x0000000117724c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-8" daemon [_thread_blocked, id=47619, stack(0x00000003151e4000,0x00000003153e7000)] + 0x000000015890a200 JavaThread "nacos-grpc-client-executor-127.0.0.1-9" daemon [_thread_blocked, id=58371, stack(0x00000003153f0000,0x00000003155f3000)] + 0x00000001270c2a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-10" daemon [_thread_blocked, id=57859, stack(0x00000003155fc000,0x00000003157ff000)] + 0x0000000158924a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-11" daemon [_thread_blocked, id=47875, stack(0x0000000315808000,0x0000000315a0b000)] + 0x00000001598d1600 JavaThread "nacos-grpc-client-executor-127.0.0.1-12" daemon [_thread_blocked, id=48387, stack(0x0000000315a14000,0x0000000315c17000)] + 0x0000000127d62e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-13" daemon [_thread_blocked, id=48899, stack(0x0000000315c20000,0x0000000315e23000)] + 0x00000001581bee00 JavaThread "nacos-grpc-client-executor-127.0.0.1-14" daemon [_thread_blocked, id=57091, stack(0x0000000315e2c000,0x000000031602f000)] + 0x0000000117718c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-15" daemon [_thread_blocked, id=56579, stack(0x0000000316038000,0x000000031623b000)] + 0x0000000147538000 JavaThread "nacos-grpc-client-executor-127.0.0.1-16" daemon [_thread_blocked, id=56067, stack(0x0000000316244000,0x0000000316447000)] + 0x0000000127da4a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-17" daemon [_thread_blocked, id=55555, stack(0x0000000316450000,0x0000000316653000)] + 0x0000000137555e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-18" daemon [_thread_blocked, id=49155, stack(0x000000031665c000,0x000000031685f000)] + 0x0000000147537a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-19" daemon [_thread_blocked, id=55055, stack(0x0000000316868000,0x0000000316a6b000)] + 0x0000000117731c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-20" daemon [_thread_blocked, id=54787, stack(0x0000000316a74000,0x0000000316c77000)] + 0x00000001374c1800 JavaThread "nacos-grpc-client-executor-127.0.0.1-21" daemon [_thread_blocked, id=54275, stack(0x0000000316c80000,0x0000000316e83000)] + 0x0000000159930000 JavaThread "nacos-grpc-client-executor-127.0.0.1-22" daemon [_thread_blocked, id=54019, stack(0x0000000316e8c000,0x000000031708f000)] +=>0x00000001178ff000 JavaThread "sentinel-datafile-log-executor-thread-1" daemon [_thread_in_native, id=36379, stack(0x000000031147c000,0x000000031167f000)] + 0x00000001055f5200 JavaThread "sentinel-command-center-executor-thread-1" daemon [_thread_in_native, id=40979, stack(0x0000000311894000,0x0000000311a97000)] + 0x00000001055e8200 JavaThread "sentinel-heartbeat-send-task-thread-1" daemon [_thread_blocked, id=40743, stack(0x0000000311688000,0x000000031188b000)] + 0x0000000147613600 JavaThread "nacos-grpc-client-executor-127.0.0.1-23" daemon [_thread_blocked, id=36651, stack(0x0000000311270000,0x0000000311473000)] + 0x0000000157615000 JavaThread "nacos-grpc-client-executor-127.0.0.1-24" daemon [_thread_blocked, id=50195, stack(0x0000000317098000,0x000000031729b000)] + 0x000000015762ea00 JavaThread "nacos-grpc-client-executor-127.0.0.1-25" daemon [_thread_blocked, id=50443, stack(0x00000003172a4000,0x00000003174a7000)] + 0x0000000127e76400 JavaThread "nacos-grpc-client-executor-127.0.0.1-26" daemon [_thread_blocked, id=50691, stack(0x00000003174b0000,0x00000003176b3000)] + 0x0000000127171400 JavaThread "nacos-grpc-client-executor-127.0.0.1-27" daemon [_thread_blocked, id=51203, stack(0x00000003176bc000,0x00000003178bf000)] + 0x000000015765fe00 JavaThread "nacos-grpc-client-executor-127.0.0.1-28" daemon [_thread_blocked, id=52995, stack(0x00000003178c8000,0x0000000317acb000)] + 0x0000000159961c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-29" daemon [_thread_blocked, id=52739, stack(0x0000000317ad4000,0x0000000317cd7000)] + 0x00000001300bb200 JavaThread "RMI TCP Connection(3)-127.0.0.1" daemon [_thread_in_native, id=32067, stack(0x0000000317ce0000,0x0000000317ee3000)] + 0x000000010576e600 JavaThread "sentinel-time-tick-thread" daemon [_thread_blocked, id=52099, stack(0x0000000340004000,0x0000000340207000)] + 0x000000012717f600 JavaThread "sentinel-heartbeat-send-task-thread-2" daemon [_thread_blocked, id=87043, stack(0x0000000340210000,0x0000000340413000)] + +Other Threads: + 0x0000000136e072e0 VMThread "VM Thread" [stack: 0x0000000175288000,0x000000017548b000] [id=18947] + 0x0000000104e18360 WatcherThread [stack: 0x0000000310e58000,0x000000031105b000] [id=41731] + 0x0000000126f05a10 GCTaskThread "GC Thread#0" [stack: 0x000000017484c000,0x0000000174a4f000] [id=14595] + 0x0000000104c0b500 GCTaskThread "GC Thread#1" [stack: 0x0000000177878000,0x0000000177a7b000] [id=26115] + 0x0000000156f07940 GCTaskThread "GC Thread#2" [stack: 0x0000000177a84000,0x0000000177c87000] [id=32771] + 0x0000000146e06230 GCTaskThread "GC Thread#3" [stack: 0x0000000177c90000,0x0000000177e93000] [id=43267] + 0x0000000104d05320 GCTaskThread "GC Thread#4" [stack: 0x0000000310004000,0x0000000310207000] [id=43011] + 0x0000000156f07dd0 GCTaskThread "GC Thread#5" [stack: 0x0000000310210000,0x0000000310413000] [id=42755] + 0x0000000104e103b0 GCTaskThread "GC Thread#6" [stack: 0x000000031041c000,0x000000031061f000] [id=34051] + 0x0000000104e10c30 GCTaskThread "GC Thread#7" [stack: 0x0000000310628000,0x000000031082b000] [id=42499] + 0x0000000104e114b0 GCTaskThread "GC Thread#8" [stack: 0x0000000310834000,0x0000000310a37000] [id=42243] + 0x0000000104e11d30 GCTaskThread "GC Thread#9" [stack: 0x0000000310a40000,0x0000000310c43000] [id=35075] + 0x0000000126e04ca0 ConcurrentGCThread "G1 Main Marker" [stack: 0x0000000174a58000,0x0000000174c5b000] [id=14339] + 0x0000000126e05530 ConcurrentGCThread "G1 Conc#0" [stack: 0x0000000174c64000,0x0000000174e67000] [id=13571] + 0x0000000104c2a5c0 ConcurrentGCThread "G1 Conc#1" [stack: 0x0000000311eb8000,0x00000003120bb000] [id=38147] + 0x0000000136e2f540 ConcurrentGCThread "G1 Conc#2" [stack: 0x00000003120c4000,0x00000003122c7000] [id=38659] + 0x0000000157806120 ConcurrentGCThread "G1 Refine#0" [stack: 0x0000000174e70000,0x0000000175073000] [id=21507] + 0x0000000126e05db0 ConcurrentGCThread "G1 Service" [stack: 0x000000017507c000,0x000000017527f000] [id=16899] + +Threads with active compile tasks: + +VM state: not at safepoint (normal execution) + +VM Mutex/Monitor currently owned by a thread: None + +Heap address: 0x00000005c0000000, size: 9216 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 + +CDS archive(s) mapped at: [0x000000f800000000-0x000000f800c14000-0x000000f800c14000), size 12664832, SharedBaseAddress: 0x000000f800000000, ArchiveRelocationMode: 1. +Compressed class space mapped at: 0x000000f801000000-0x000000f841000000, reserved size: 1073741824 +Narrow klass base: 0x000000f800000000, Narrow klass shift: 0, Narrow klass range: 0x100000000 + +GC Precious Log: + CPUs: 12 total, 12 available + Memory: 36864M + Large Page Support: Disabled + NUMA Support: Disabled + Compressed Oops: Enabled (Zero based) + Heap Region Size: 8M + Heap Min Capacity: 8M + Heap Initial Capacity: 576M + Heap Max Capacity: 9G + Pre-touch: Disabled + Parallel Workers: 10 + Concurrent Workers: 3 + Concurrent Refinement Workers: 10 + Periodic GC: Disabled + +Heap: + garbage-first heap total 221184K, used 70377K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 4 young (32768K), 2 survivors (16384K) + Metaspace used 60876K, committed 61312K, reserved 1114112K + class space used 7924K, committed 8128K, reserved 1048576K + +Heap Regions: E=young(eden), S=young(survivor), O=old, HS=humongous(starts), HC=humongous(continues), CS=collection set, F=free, OA=open archive, CA=closed archive, TAMS=top-at-mark-start (previous, next) +| 0|0x00000005c0000000, 0x00000005c0800000, 0x00000005c0800000|100%| O| |TAMS 0x00000005c0800000, 0x00000005c0000000| Untracked +| 1|0x00000005c0800000, 0x00000005c1000000, 0x00000005c1000000|100%| O| |TAMS 0x00000005c1000000, 0x00000005c0800000| Untracked +| 2|0x00000005c1000000, 0x00000005c1782e00, 0x00000005c1800000| 93%| O| |TAMS 0x00000005c1782e00, 0x00000005c1000000| Untracked +| 3|0x00000005c1800000, 0x00000005c2000000, 0x00000005c2000000|100%| O| |TAMS 0x00000005c2000000, 0x00000005c1800000| Untracked +| 4|0x00000005c2000000, 0x00000005c2800000, 0x00000005c2800000|100%| O| |TAMS 0x00000005c2800000, 0x00000005c2000000| Untracked +| 5|0x00000005c2800000, 0x00000005c2b4da00, 0x00000005c3000000| 41%| O| |TAMS 0x00000005c2b4da00, 0x00000005c2800000| Untracked +| 6|0x00000005c3000000, 0x00000005c3000000, 0x00000005c3800000| 0%| F| |TAMS 0x00000005c3000000, 0x00000005c3000000| Untracked +| 7|0x00000005c3800000, 0x00000005c3800000, 0x00000005c4000000| 0%| F| |TAMS 0x00000005c3800000, 0x00000005c3800000| Untracked +| 8|0x00000005c4000000, 0x00000005c4000000, 0x00000005c4800000| 0%| F| |TAMS 0x00000005c4000000, 0x00000005c4000000| Untracked +| 9|0x00000005c4800000, 0x00000005c4800000, 0x00000005c5000000| 0%| F| |TAMS 0x00000005c4800000, 0x00000005c4800000| Untracked +| 10|0x00000005c5000000, 0x00000005c5000000, 0x00000005c5800000| 0%| F| |TAMS 0x00000005c5000000, 0x00000005c5000000| Untracked +| 11|0x00000005c5800000, 0x00000005c5800000, 0x00000005c6000000| 0%| F| |TAMS 0x00000005c5800000, 0x00000005c5800000| Untracked +| 12|0x00000005c6000000, 0x00000005c6000000, 0x00000005c6800000| 0%| F| |TAMS 0x00000005c6000000, 0x00000005c6000000| Untracked +| 13|0x00000005c6800000, 0x00000005c6800000, 0x00000005c7000000| 0%| F| |TAMS 0x00000005c6800000, 0x00000005c6800000| Untracked +| 14|0x00000005c7000000, 0x00000005c7000000, 0x00000005c7800000| 0%| F| |TAMS 0x00000005c7000000, 0x00000005c7000000| Untracked +| 15|0x00000005c7800000, 0x00000005c7800000, 0x00000005c8000000| 0%| F| |TAMS 0x00000005c7800000, 0x00000005c7800000| Untracked +| 16|0x00000005c8000000, 0x00000005c81f1d58, 0x00000005c8800000| 24%| S|CS|TAMS 0x00000005c8000000, 0x00000005c8000000| Complete +| 17|0x00000005c8800000, 0x00000005c9000000, 0x00000005c9000000|100%| S|CS|TAMS 0x00000005c8800000, 0x00000005c8800000| Complete +| 18|0x00000005c9000000, 0x00000005c9000000, 0x00000005c9800000| 0%| F| |TAMS 0x00000005c9000000, 0x00000005c9000000| Untracked +| 19|0x00000005c9800000, 0x00000005c9800000, 0x00000005ca000000| 0%| F| |TAMS 0x00000005c9800000, 0x00000005c9800000| Untracked +| 20|0x00000005ca000000, 0x00000005ca000000, 0x00000005ca800000| 0%| F| |TAMS 0x00000005ca000000, 0x00000005ca000000| Untracked +| 21|0x00000005ca800000, 0x00000005ca800000, 0x00000005cb000000| 0%| F| |TAMS 0x00000005ca800000, 0x00000005ca800000| Untracked +| 22|0x00000005cb000000, 0x00000005cb000000, 0x00000005cb800000| 0%| F| |TAMS 0x00000005cb000000, 0x00000005cb000000| Untracked +| 23|0x00000005cb800000, 0x00000005cbe5add0, 0x00000005cc000000| 79%| E| |TAMS 0x00000005cb800000, 0x00000005cb800000| Complete +| 71|0x00000005e3800000, 0x00000005e4000000, 0x00000005e4000000|100%| E|CS|TAMS 0x00000005e3800000, 0x00000005e3800000| Complete +|1150|0x00000007ff000000, 0x00000007ff778000, 0x00000007ff800000| 93%|OA| |TAMS 0x00000007ff778000, 0x00000007ff000000| Untracked +|1151|0x00000007ff800000, 0x00000007ff880000, 0x0000000800000000| 6%|CA| |TAMS 0x00000007ff880000, 0x00000007ff800000| Untracked + +Card table byte_map: [0x000000011289c000,0x0000000113a9c000] _byte_map_base: 0x000000010fa9c000 + +Marking Bits (Prev, Next): (CMBitMap*) 0x0000000127011250, (CMBitMap*) 0x0000000127011210 + Prev Bits: [0x000000016b848000, 0x0000000174848000) + Next Bits: [0x000000015a000000, 0x0000000163000000) + +Polling page: 0x00000001048e0000 + +Metaspace: + +Usage: + Non-class: 51.71 MB used. + Class: 7.74 MB used. + Both: 59.45 MB used. + +Virtual space: + Non-class space: 64.00 MB reserved, 51.94 MB ( 81%) committed, 1 nodes. + Class space: 1.00 GB reserved, 7.94 MB ( <1%) committed, 1 nodes. + Both: 1.06 GB reserved, 59.88 MB ( 6%) committed. + +Chunk freelists: + Non-Class: 11.69 MB + Class: 8.03 MB + Both: 19.72 MB + +MaxMetaspaceSize: unlimited +CompressedClassSpaceSize: 1.00 GB +Initial GC threshold: 21.00 MB +Current GC threshold: 98.25 MB +CDS: on +MetaspaceReclaimPolicy: balanced + - commit_granule_bytes: 65536. + - commit_granule_words: 8192. + - virtual_space_node_default_size: 8388608. + - enlarge_chunks_in_place: 1. + - new_chunks_are_fully_committed: 0. + - uncommit_free_chunks: 1. + - use_allocation_guard: 0. + - handle_deallocations: 1. + + +Internal statistics: + +num_allocs_failed_limit: 9. +num_arena_births: 620. +num_arena_deaths: 4. +num_vsnodes_births: 2. +num_vsnodes_deaths: 0. +num_space_committed: 958. +num_space_uncommitted: 0. +num_chunks_returned_to_freelist: 13. +num_chunks_taken_from_freelist: 2511. +num_chunk_merges: 9. +num_chunk_splits: 1878. +num_chunks_enlarged: 1524. +num_inconsistent_stats: 0. + +CodeCache: size=49152Kb used=12288Kb max_used=12288Kb free=36863Kb + bounds [0x000000010e69c000, 0x000000010f2ac000, 0x000000011169c000] + total_blobs=6256 nmethods=5630 adapters=553 + compilation: enabled + stopped_count=0, restarted_count=0 + full_count=0 + +Compilation events (20 events): +Event: 8.861 Thread 0x000000012701e000 5881 1 org.springframework.beans.factory.support.DefaultListableBeanFactory::getBeanNamesForType (101 bytes) +Event: 8.861 Thread 0x000000012701e000 nmethod 5881 0x000000010f296b10 code [0x000000010f296d00, 0x000000010f297178] +Event: 8.873 Thread 0x0000000137023e00 5882 1 org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec::getStrategy (18 bytes) +Event: 8.873 Thread 0x0000000137023e00 nmethod 5882 0x000000010f297410 code [0x000000010f297580, 0x000000010f297658] +Event: 8.873 Thread 0x000000012701e000 5883 1 org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec::getParameterizedContainers (5 bytes) +Event: 8.873 Thread 0x0000000105010a00 5884 1 org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec::getIgnoredTypes (5 bytes) +Event: 8.873 Thread 0x000000012701e000 nmethod 5883 0x000000010f297710 code [0x000000010f297880, 0x000000010f297918] +Event: 8.873 Thread 0x0000000105010a00 nmethod 5884 0x000000010f297a10 code [0x000000010f297b80, 0x000000010f297c18] +Event: 8.884 Thread 0x0000000117820600 5885 1 java.security.BasicPermission::init (132 bytes) +Event: 8.884 Thread 0x0000000117820600 nmethod 5885 0x000000010f297d10 code [0x000000010f297f80, 0x000000010f298658] +Event: 8.909 Thread 0x0000000137023e00 5887 1 org.springframework.context.annotation.AnnotationScopeMetadataResolver::resolveScopeMetadata (85 bytes) +Event: 8.909 Thread 0x0000000105010a00 5888 1 org.springframework.context.annotation.ScopeMetadata:: (18 bytes) +Event: 8.909 Thread 0x0000000105010a00 nmethod 5888 0x000000010f298b10 code [0x000000010f298cc0, 0x000000010f298e58] +Event: 8.909 Thread 0x000000012701e000 5889 1 org.springframework.core.annotation.TypeMappedAnnotation::getClassLoader (70 bytes) +Event: 8.909 Thread 0x0000000137023e00 nmethod 5887 0x000000010f298f10 code [0x000000010f299180, 0x000000010f299818] +Event: 8.910 Thread 0x000000012701e000 nmethod 5889 0x000000010f299c90 code [0x000000010f299ec0, 0x000000010f29a478] +Event: 8.911 Thread 0x0000000137023e00 5890 1 org.springframework.beans.factory.support.DefaultListableBeanFactory::getBeanNamesForType (35 bytes) +Event: 8.912 Thread 0x0000000137023e00 nmethod 5890 0x000000010f29a890 code [0x000000010f29aa40, 0x000000010f29ac38] +Event: 8.927 Thread 0x000000012701e000 5893 1 java.util.regex.Pattern::qtype (39 bytes) +Event: 8.927 Thread 0x000000012701e000 nmethod 5893 0x000000010f29b990 code [0x000000010f29bb80, 0x000000010f29beb8] + +GC Heap History (20 events): +Event: 1.161 GC heap before +{Heap before GC invocations=3 (full 0): + garbage-first heap total 606208K, used 83747K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 7 young (57344K), 1 survivors (8192K) + Metaspace used 15618K, committed 15936K, reserved 1114112K + class space used 1924K, committed 2048K, reserved 1048576K +} +Event: 1.164 GC heap after +{Heap after GC invocations=4 (full 0): + garbage-first heap total 606208K, used 35635K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 15618K, committed 15936K, reserved 1114112K + class space used 1924K, committed 2048K, reserved 1048576K +} +Event: 1.392 GC heap before +{Heap before GC invocations=4 (full 0): + garbage-first heap total 606208K, used 76595K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 7 young (57344K), 1 survivors (8192K) + Metaspace used 21245K, committed 21504K, reserved 1114112K + class space used 2660K, committed 2752K, reserved 1048576K +} +Event: 1.393 GC heap after +{Heap after GC invocations=5 (full 0): + garbage-first heap total 606208K, used 37226K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 21245K, committed 21504K, reserved 1114112K + class space used 2660K, committed 2752K, reserved 1048576K +} +Event: 2.427 GC heap before +{Heap before GC invocations=6 (full 0): + garbage-first heap total 196608K, used 143722K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 14 young (114688K), 1 survivors (8192K) + Metaspace used 31866K, committed 32256K, reserved 1114112K + class space used 3982K, committed 4160K, reserved 1048576K +} +Event: 2.441 GC heap after +{Heap after GC invocations=7 (full 0): + garbage-first heap total 196608K, used 39539K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 31866K, committed 32256K, reserved 1114112K + class space used 3982K, committed 4160K, reserved 1048576K +} +Event: 2.521 GC heap before +{Heap before GC invocations=7 (full 0): + garbage-first heap total 196608K, used 47731K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 1 survivors (8192K) + Metaspace used 32583K, committed 33024K, reserved 1114112K + class space used 4074K, committed 4288K, reserved 1048576K +} +Event: 2.527 GC heap after +{Heap after GC invocations=8 (full 0): + garbage-first heap total 196608K, used 37869K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 32583K, committed 33024K, reserved 1114112K + class space used 4074K, committed 4288K, reserved 1048576K +} +Event: 2.683 GC heap before +{Heap before GC invocations=8 (full 0): + garbage-first heap total 196608K, used 62445K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 5 young (40960K), 1 survivors (8192K) + Metaspace used 35765K, committed 36096K, reserved 1114112K + class space used 4463K, committed 4608K, reserved 1048576K +} +Event: 2.686 GC heap after +{Heap after GC invocations=9 (full 0): + garbage-first heap total 245760K, used 38769K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 35765K, committed 36096K, reserved 1114112K + class space used 4463K, committed 4608K, reserved 1048576K +} +Event: 4.142 GC heap before +{Heap before GC invocations=10 (full 0): + garbage-first heap total 221184K, used 161649K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 16 young (131072K), 1 survivors (8192K) + Metaspace used 47506K, committed 47936K, reserved 1114112K + class space used 6180K, committed 6400K, reserved 1048576K +} +Event: 4.163 GC heap after +{Heap after GC invocations=11 (full 0): + garbage-first heap total 221184K, used 42475K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 2 survivors (16384K) + Metaspace used 47506K, committed 47936K, reserved 1114112K + class space used 6180K, committed 6400K, reserved 1048576K +} +Event: 4.432 GC heap before +{Heap before GC invocations=11 (full 0): + garbage-first heap total 221184K, used 50667K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 3 young (24576K), 2 survivors (16384K) + Metaspace used 48268K, committed 48704K, reserved 1114112K + class space used 6299K, committed 6528K, reserved 1048576K +} +Event: 4.450 GC heap after +{Heap after GC invocations=12 (full 0): + garbage-first heap total 221184K, used 43614K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 48268K, committed 48704K, reserved 1114112K + class space used 6299K, committed 6528K, reserved 1048576K +} +Event: 5.741 GC heap before +{Heap before GC invocations=12 (full 0): + garbage-first heap total 221184K, used 150110K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 15 young (122880K), 1 survivors (8192K) + Metaspace used 51028K, committed 51456K, reserved 1114112K + class space used 6663K, committed 6848K, reserved 1048576K +} +Event: 5.746 GC heap after +{Heap after GC invocations=13 (full 0): + garbage-first heap total 221184K, used 53612K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 2 survivors (16384K) + Metaspace used 51028K, committed 51456K, reserved 1114112K + class space used 6663K, committed 6848K, reserved 1048576K +} +Event: 7.882 GC heap before +{Heap before GC invocations=13 (full 0): + garbage-first heap total 221184K, used 160108K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 15 young (122880K), 2 survivors (16384K) + Metaspace used 58340K, committed 58816K, reserved 1114112K + class space used 7527K, committed 7744K, reserved 1048576K +} +Event: 7.898 GC heap after +{Heap after GC invocations=14 (full 0): + garbage-first heap total 221184K, used 58444K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 58340K, committed 58816K, reserved 1114112K + class space used 7527K, committed 7744K, reserved 1048576K +} +Event: 8.518 GC heap before +{Heap before GC invocations=14 (full 0): + garbage-first heap total 221184K, used 107596K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 8 young (65536K), 1 survivors (8192K) + Metaspace used 59896K, committed 60288K, reserved 1114112K + class space used 7761K, committed 7936K, reserved 1048576K +} +Event: 8.521 GC heap after +{Heap after GC invocations=15 (full 0): + garbage-first heap total 221184K, used 62185K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 2 survivors (16384K) + Metaspace used 59896K, committed 60288K, reserved 1114112K + class space used 7761K, committed 7936K, reserved 1048576K +} + +Dll operation events (11 events): +Event: 0.018 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjava.dylib +Event: 0.019 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +Event: 0.090 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libinstrument.dylib +Event: 0.092 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnio.dylib +Event: 0.095 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnet.dylib +Event: 0.119 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjimage.dylib +Event: 0.128 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +Event: 0.236 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement.dylib +Event: 0.245 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement_ext.dylib +Event: 0.377 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libextnet.dylib +Event: 7.955 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libverify.dylib + +Deoptimization events (20 events): +Event: 8.649 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010eb95348 sp=0x000000016b841cf0 +Event: 8.649 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b8419d0 mode 1 +Event: 8.649 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010ef28ad0 sp=0x000000016b841d90 +Event: 8.649 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b841b10 mode 1 +Event: 8.650 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010eb078a8 sp=0x000000016b841480 +Event: 8.651 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b841120 mode 1 +Event: 8.651 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010eb06b3c sp=0x000000016b841550 +Event: 8.651 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b841220 mode 1 +Event: 8.651 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010eb95348 sp=0x000000016b841cf0 +Event: 8.651 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b8419d0 mode 1 +Event: 8.651 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010ef28ad0 sp=0x000000016b841d90 +Event: 8.651 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b841b10 mode 1 +Event: 8.652 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010eb078a8 sp=0x000000016b841480 +Event: 8.652 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b841120 mode 1 +Event: 8.652 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010eb06b3c sp=0x000000016b841550 +Event: 8.652 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b841220 mode 1 +Event: 8.652 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010eb95348 sp=0x000000016b841cf0 +Event: 8.652 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b8419d0 mode 1 +Event: 8.652 Thread 0x0000000127008600 DEOPT PACKING pc=0x000000010ef28ad0 sp=0x000000016b841d90 +Event: 8.652 Thread 0x0000000127008600 DEOPT UNPACKING pc=0x000000010e6e377c sp=0x000000016b841b10 mode 1 + +Classes unloaded (2 events): +Event: 8.535 Thread 0x0000000136e072e0 Unloading class 0x000000f801664800 'SC' +Event: 8.535 Thread 0x0000000136e072e0 Unloading class 0x000000f801554000 'SC' + +Classes redefined (1 events): +Event: 0.110 Thread 0x0000000136e072e0 redefined class name=java.lang.Throwable, count=1 + +Internal exceptions (20 events): +Event: 4.426 Thread 0x0000000127008600 Exception (0x00000005e3fa79a8) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 4.563 Thread 0x0000000147623c00 Exception (0x00000005cb43d600) +thrown [src/hotspot/share/prims/jni.cpp, line 535] +Event: 4.599 Thread 0x00000001473b0800 Exception (0x00000005cb7d5300) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 4.677 Thread 0x0000000127008600 Exception (0x00000005ca98d140) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 826] +Event: 4.697 Thread 0x0000000127008600 Exception (0x00000005caa29b78) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 5.103 Thread 0x00000001473b0800 Exception (0x00000005cb7e1d70) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 5.448 Thread 0x0000000127008600 Exception (0x00000005c58f6100) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 826] +Event: 5.616 Thread 0x00000001473b0800 Exception (0x00000005c5541730) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 6.121 Thread 0x00000001473b0800 Exception (0x00000005caee8ad8) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 6.550 Thread 0x0000000127008600 Exception (0x00000005c90aba90) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 826] +Event: 6.627 Thread 0x00000001473b0800 Exception (0x00000005c952e2e8) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 7.132 Thread 0x00000001176d7800 Exception (0x00000005c89f7490) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 7.198 Thread 0x0000000127008600 Exception (0x00000005c84b3350) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.198 Thread 0x0000000127008600 Exception (0x00000005c84b9da0) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.199 Thread 0x0000000127008600 Exception (0x00000005c84bdd98) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.211 Thread 0x0000000127008600 Exception (0x00000005c8555d28) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.637 Thread 0x00000001176d7800 Exception (0x00000005c8a03c48) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 8.139 Thread 0x00000001176d7800 Exception (0x00000005c980a0c0) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 8.643 Thread 0x00000001176d7800 Exception (0x00000005e3e4d110) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 8.927 Thread 0x00000001055e8200 Exception (0x00000005cbe046d0) +thrown [src/hotspot/share/prims/jni.cpp, line 516] + +VM Operations (20 events): +Event: 5.825 Executing VM operation: HandshakeAllThreads +Event: 5.825 Executing VM operation: HandshakeAllThreads done +Event: 6.540 Executing VM operation: ICBufferFull +Event: 6.540 Executing VM operation: ICBufferFull done +Event: 6.580 Executing VM operation: HandshakeAllThreads +Event: 6.580 Executing VM operation: HandshakeAllThreads done +Event: 7.253 Executing VM operation: HandshakeAllThreads +Event: 7.253 Executing VM operation: HandshakeAllThreads done +Event: 7.456 Executing VM operation: HandshakeAllThreads +Event: 7.456 Executing VM operation: HandshakeAllThreads done +Event: 7.704 Executing VM operation: ICBufferFull +Event: 7.704 Executing VM operation: ICBufferFull done +Event: 7.881 Executing VM operation: G1CollectForAllocation +Event: 7.898 Executing VM operation: G1CollectForAllocation done +Event: 8.517 Executing VM operation: CollectForMetadataAllocation +Event: 8.521 Executing VM operation: CollectForMetadataAllocation done +Event: 8.534 Executing VM operation: G1PauseRemark +Event: 8.542 Executing VM operation: G1PauseRemark done +Event: 8.548 Executing VM operation: G1PauseCleanup +Event: 8.549 Executing VM operation: G1PauseCleanup done + +Events (20 events): +Event: 8.927 loading class sun/net/util/SocketExceptions done +Event: 8.928 Thread 0x000000012717f600 Thread added: 0x000000012717f600 +Event: 8.928 Protecting memory [0x0000000340210000,0x000000034021c000] with protection modes 0 +Event: 8.928 loading class java/lang/Throwable$WrappedPrintWriter +Event: 8.928 loading class java/lang/Throwable$WrappedPrintWriter done +Event: 8.928 loading class com/intellij/rt/debugger/agent/CaptureStorage$9 +Event: 8.928 loading class com/intellij/rt/debugger/agent/CaptureStorage$Callable +Event: 8.928 loading class com/intellij/rt/debugger/agent/CaptureStorage$Callable done +Event: 8.928 loading class com/intellij/rt/debugger/agent/CaptureStorage$9 done +Event: 8.928 loading class com/intellij/rt/debugger/agent/CaptureStorage$DeepCapturedStack +Event: 8.928 loading class com/intellij/rt/debugger/agent/CaptureStorage$DeepCapturedStack done +Event: 8.928 loading class jdk/internal/loader/BootLoader$PackageHelper$1 +Event: 8.928 loading class jdk/internal/loader/BootLoader$PackageHelper$1 done +Event: 8.928 loading class jdk/internal/loader/BootLoader$PackageHelper$2 +Event: 8.928 loading class jdk/internal/loader/BootLoader$PackageHelper$2 done +Event: 8.928 loading class java/util/jar/JarInputStream +Event: 8.928 loading class java/util/zip/ZipInputStream +Event: 8.929 loading class java/util/zip/ZipInputStream done +Event: 8.929 loading class java/util/jar/JarInputStream done +Event: 8.929 loading class com/intellij/rt/debugger/agent/CaptureStorage$StackData + + +Dynamic libraries: +0x0000000104880000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjli.dylib +0x0000000195854000 /usr/lib/libz.1.dylib +0x000000019590a000 /usr/lib/libSystem.B.dylib +0x0000000195904000 /usr/lib/system/libcache.dylib +0x00000001958bf000 /usr/lib/system/libcommonCrypto.dylib +0x00000001958ea000 /usr/lib/system/libcompiler_rt.dylib +0x00000001958df000 /usr/lib/system/libcopyfile.dylib +0x00000001857f2000 /usr/lib/system/libcorecrypto.dylib +0x00000001858f2000 /usr/lib/system/libdispatch.dylib +0x000000018568f000 /usr/lib/system/libdyld.dylib +0x00000001958fa000 /usr/lib/system/libkeymgr.dylib +0x00000001958a2000 /usr/lib/system/libmacho.dylib +0x0000000194b35000 /usr/lib/system/libquarantine.dylib +0x00000001958f7000 /usr/lib/system/libremovefile.dylib +0x000000018c265000 /usr/lib/system/libsystem_asl.dylib +0x0000000185778000 /usr/lib/system/libsystem_blocks.dylib +0x000000018593d000 /usr/lib/system/libsystem_c.dylib +0x00000001958ee000 /usr/lib/system/libsystem_collections.dylib +0x00000001934d5000 /usr/lib/system/libsystem_configuration.dylib +0x00000001920c3000 /usr/lib/system/libsystem_containermanager.dylib +0x00000001952d4000 /usr/lib/system/libsystem_coreservices.dylib +0x0000000189a8c000 /usr/lib/system/libsystem_darwin.dylib +0x000000028b4e0000 /usr/lib/system/libsystem_darwindirectory.dylib +0x00000001958fb000 /usr/lib/system/libsystem_dnssd.dylib +0x000000028b4e4000 /usr/lib/system/libsystem_eligibility.dylib +0x000000018593a000 /usr/lib/system/libsystem_featureflags.dylib +0x0000000185abf000 /usr/lib/system/libsystem_info.dylib +0x0000000195863000 /usr/lib/system/libsystem_m.dylib +0x00000001858a1000 /usr/lib/system/libsystem_malloc.dylib +0x000000018c1c8000 /usr/lib/system/libsystem_networkextension.dylib +0x0000000189ef7000 /usr/lib/system/libsystem_notify.dylib +0x00000001934da000 /usr/lib/system/libsystem_sandbox.dylib +0x000000028b4ef000 /usr/lib/system/libsystem_sanitizers.dylib +0x00000001958f3000 /usr/lib/system/libsystem_secinit.dylib +0x0000000185a6b000 /usr/lib/system/libsystem_kernel.dylib +0x0000000185ab6000 /usr/lib/system/libsystem_platform.dylib +0x0000000185aa9000 /usr/lib/system/libsystem_pthread.dylib +0x000000018de1e000 /usr/lib/system/libsystem_symptoms.dylib +0x00000001857d1000 /usr/lib/system/libsystem_trace.dylib +0x000000028b4f7000 /usr/lib/system/libsystem_trial.dylib +0x00000001958cd000 /usr/lib/system/libunwind.dylib +0x000000018577c000 /usr/lib/system/libxpc.dylib +0x000000018563c000 /usr/lib/libobjc.A.dylib +0x0000000185aef000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation +0x00000001991ff000 /usr/lib/swift/libswiftCore.dylib +0x0000000185a50000 /usr/lib/libc++abi.dylib +0x00000002898cd000 /usr/lib/libRosetta.dylib +0x00000001859bf000 /usr/lib/libc++.1.dylib +0x000000018735e000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation +0x00000001a2ddf000 /usr/lib/swift/libswiftObjectiveC.dylib +0x000000028ad49000 /usr/lib/libswiftPrespecialized.dylib +0x0000000186fcd000 /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration +0x000000019033f000 /System/Library/PrivateFrameworks/CoreAutoLayout.framework/Versions/A/CoreAutoLayout +0x000000019590c000 /usr/lib/libfakelink.dylib +0x0000000195bb5000 /usr/lib/libcompression.dylib +0x000000018be12000 /System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork +0x000000018f770000 /System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration +0x000000019595f000 /usr/lib/libarchive.2.dylib +0x000000018f675000 /usr/lib/libDiagnosticMessagesClient.dylib +0x00000001897b6000 /usr/lib/libicucore.A.dylib +0x0000000190388000 /usr/lib/libxml2.2.dylib +0x000000019e08e000 /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices +0x00000001934e8000 /usr/lib/liblangid.dylib +0x0000000189e0e000 /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit +0x000000019bd00000 /System/Library/Frameworks/Combine.framework/Versions/A/Combine +0x000000023ec2f000 /System/Library/PrivateFrameworks/CollectionsInternal.framework/Versions/A/CollectionsInternal +0x000000026ac75000 /System/Library/PrivateFrameworks/ReflectionInternal.framework/Versions/A/ReflectionInternal +0x000000026bbc9000 /System/Library/PrivateFrameworks/RuntimeInternal.framework/Versions/A/RuntimeInternal +0x000000019590e000 /System/Library/PrivateFrameworks/SoftLinking.framework/Versions/A/SoftLinking +0x00000001b34c8000 /usr/lib/swift/libswiftCoreFoundation.dylib +0x00000001b028b000 /usr/lib/swift/libswiftDarwin.dylib +0x000000019fe05000 /usr/lib/swift/libswiftDispatch.dylib +0x00000001b3529000 /usr/lib/swift/libswiftIOKit.dylib +0x000000028b18c000 /usr/lib/swift/libswiftSystem.dylib +0x00000001b34db000 /usr/lib/swift/libswiftXPC.dylib +0x000000028b1be000 /usr/lib/swift/libswift_Builtin_float.dylib +0x000000028b1bf000 /usr/lib/swift/libswift_Concurrency.dylib +0x000000028b24b000 /usr/lib/swift/libswift_DarwinFoundation1.dylib +0x000000028b2ef000 /usr/lib/swift/libswift_StringProcessing.dylib +0x00000001a2de3000 /usr/lib/swift/libswiftos.dylib +0x0000000189d8e000 /System/Library/PrivateFrameworks/CoreServicesInternal.framework/Versions/A/CoreServicesInternal +0x00000001958d7000 /usr/lib/liboah.dylib +0x0000000189396000 /System/Library/Frameworks/Security.framework/Versions/A/Security +0x00000001a2213000 /System/Library/PrivateFrameworks/DiskImages.framework/Versions/A/DiskImages +0x00000001afd2f000 /System/Library/Frameworks/NetFS.framework/Versions/A/NetFS +0x0000000190304000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents +0x0000000189a96000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore +0x000000018f6e4000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata +0x00000001952db000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices +0x0000000195a57000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit +0x000000018dd98000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE +0x000000018604e000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices +0x0000000196e60000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices +0x0000000190311000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList +0x0000000195aea000 /usr/lib/libapple_nghttp2.dylib +0x000000018d9b4000 /usr/lib/libsqlite3.dylib +0x000000018db9d000 /System/Library/Frameworks/Accounts.framework/Versions/A/Accounts +0x00000001a2455000 /System/Library/PrivateFrameworks/AppSupport.framework/Versions/A/AppSupport +0x00000001b227a000 /System/Library/Frameworks/AVFoundation.framework/Versions/A/AVFoundation +0x000000018f645000 /System/Library/PrivateFrameworks/CoreAnalytics.framework/Versions/A/CoreAnalytics +0x000000018c858000 /System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics +0x0000000199f3a000 /System/Library/Frameworks/GSS.framework/Versions/A/GSS +0x0000000198322000 /System/Library/PrivateFrameworks/InternationalSupport.framework/Versions/A/InternationalSupport +0x000000018dd2c000 /System/Library/PrivateFrameworks/RunningBoardServices.framework/Versions/A/RunningBoardServices +0x00000001a2d67000 /System/Library/PrivateFrameworks/StreamingZip.framework/Versions/A/StreamingZip +0x000000018c1e3000 /usr/lib/libenergytrace.dylib +0x000000018de27000 /System/Library/Frameworks/Network.framework/Versions/A/Network +0x0000000194b5d000 /usr/lib/libbsm.0.dylib +0x00000001958a6000 /usr/lib/system/libkxld.dylib +0x000000023a201000 /System/Library/PrivateFrameworks/AppleKeyStore.framework/Versions/A/AppleKeyStore +0x00000002895cf000 /usr/lib/libCoreEntitlements.dylib +0x000000025f341000 /System/Library/PrivateFrameworks/MessageSecurity.framework/Versions/A/MessageSecurity +0x000000018d998000 /System/Library/PrivateFrameworks/ProtocolBuffer.framework/Versions/A/ProtocolBuffer +0x000000019f214000 /System/Library/PrivateFrameworks/SymptomDiagnosticReporter.framework/Versions/A/SymptomDiagnosticReporter +0x00000001970a5000 /System/Library/PrivateFrameworks/CrashReporterSupport.framework/Versions/A/CrashReporterSupport +0x000000018c1e5000 /usr/lib/libMobileGestalt.dylib +0x00000001952bb000 /System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression +0x0000000194b45000 /usr/lib/libcoretls.dylib +0x0000000196ed6000 /usr/lib/libcoretls_cfhelpers.dylib +0x0000000195baf000 /usr/lib/libpam.2.dylib +0x0000000196f4c000 /usr/lib/libxar.1.dylib +0x0000000196ed8000 /System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS +0x000000027734f000 /System/Library/PrivateFrameworks/SwiftASN1Internal.framework/Versions/A/SwiftASN1Internal +0x0000000196f5b000 /usr/lib/libutil.dylib +0x00000001934e3000 /System/Library/PrivateFrameworks/AppleSystemInfo.framework/Versions/A/AppleSystemInfo +0x000000019480c000 /System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/Versions/A/IOMobileFramebuffer +0x00000001920fc000 /System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface +0x00000001a1b73000 /System/Library/PrivateFrameworks/CoreWiFi.framework/Versions/A/CoreWiFi +0x00000001b3388000 /System/Library/PrivateFrameworks/LoggingSupport.framework/Versions/A/LoggingSupport +0x0000000199f9d000 /System/Library/PrivateFrameworks/MobileAsset.framework/Versions/A/MobileAsset +0x000000019f224000 /System/Library/PrivateFrameworks/PowerLog.framework/Versions/A/PowerLog +0x00000001a06e6000 /System/Library/PrivateFrameworks/Rapport.framework/Versions/A/Rapport +0x0000000232daa000 /System/Library/Frameworks/SwiftData.framework/Versions/A/SwiftData +0x000000018b846000 /System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers +0x0000000190531000 /System/Library/PrivateFrameworks/UserManagement.framework/Versions/A/UserManagement +0x000000018bd39000 /usr/lib/libboringssl.dylib +0x000000018de0c000 /usr/lib/libdns_services.dylib +0x00000001b23ae000 /usr/lib/libquic.dylib +0x0000000199190000 /usr/lib/libusrtcp.dylib +0x000000023b0bb000 /System/Library/PrivateFrameworks/AtomicsInternal.framework/Versions/A/AtomicsInternal +0x00000001d976e000 /System/Library/PrivateFrameworks/InternalSwiftProtobuf.framework/Versions/A/InternalSwiftProtobuf +0x000000028b013000 /usr/lib/swift/libswiftDistributed.dylib +0x000000028b03c000 /usr/lib/swift/libswiftObservation.dylib +0x000000028b178000 /usr/lib/swift/libswiftSynchronization.dylib +0x00000001934e1000 /System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary +0x000000023b8eb000 /System/Library/PrivateFrameworks/BiomeLibrary.framework/Versions/A/BiomeLibrary +0x00000001c2511000 /System/Library/PrivateFrameworks/BiomeStreams.framework/Versions/A/BiomeStreams +0x00000001be560000 /System/Library/PrivateFrameworks/BiomeFoundation.framework/Versions/A/BiomeFoundation +0x00000001c87be000 /System/Library/PrivateFrameworks/BiomePubSub.framework/Versions/A/BiomePubSub +0x000000018d5aa000 /System/Library/Frameworks/CoreData.framework/Versions/A/CoreData +0x00000001a3d4a000 /System/Library/PrivateFrameworks/ProactiveSupport.framework/Versions/A/ProactiveSupport +0x0000000234e08000 /System/Library/Frameworks/_LocationEssentials.framework/Versions/A/_LocationEssentials +0x0000000196eb7000 /usr/lib/liblzma.5.dylib +0x000000019e30d000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate +0x0000000194a3e000 /System/Library/PrivateFrameworks/MobileKeyBag.framework/Versions/A/MobileKeyBag +0x00000001a2769000 /System/Library/PrivateFrameworks/InternationalTextSearch.framework/Versions/A/InternationalTextSearch +0x00000001baa33000 /System/Library/PrivateFrameworks/SoftwareUpdateCoreSupport.framework/Versions/A/SoftwareUpdateCoreSupport +0x00000001c2fb0000 /System/Library/PrivateFrameworks/SoftwareUpdateCoreConnect.framework/Versions/A/SoftwareUpdateCoreConnect +0x00000001a230d000 /System/Library/PrivateFrameworks/RemoteServiceDiscovery.framework/Versions/A/RemoteServiceDiscovery +0x00000001ba60a000 /System/Library/PrivateFrameworks/MSUDataAccessor.framework/Versions/A/MSUDataAccessor +0x00000001b555b000 /usr/lib/libbootpolicy.dylib +0x00000001a2324000 /System/Library/PrivateFrameworks/RemoteXPC.framework/Versions/A/RemoteXPC +0x00000001c23e5000 /usr/lib/libFDR.dylib +0x00000001c83c0000 /usr/lib/libamsupport.dylib +0x00000002898c5000 /usr/lib/libReverseProxyDevice.dylib +0x0000000239a6f000 /System/Library/PrivateFrameworks/AppleDeviceQuerySupport.framework/Versions/A/AppleDeviceQuerySupport +0x00000001cb58a000 /usr/lib/libpartition2_dynamic.dylib +0x0000000195ac6000 /System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce +0x000000028947a000 /usr/lib/libAppleArchive.dylib +0x00000001952c7000 /usr/lib/libbz2.1.0.dylib +0x000000018f77a000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage +0x000000019e069000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib +0x0000000196f92000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib +0x0000000186552000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib +0x00000001a2768000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices +0x000000019046f000 /System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo +0x000000018cfae000 /System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync +0x00000001889d6000 /System/Library/Frameworks/CoreText.framework/Versions/A/CoreText +0x0000000192bef000 /System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO +0x0000000199b4a000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS +0x000000018d156000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices +0x0000000198218000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore +0x0000000199f03000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD +0x0000000199efe000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy +0x0000000199b1c000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis +0x000000018c2a1000 /System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight +0x00000001925ca000 /System/Library/PrivateFrameworks/FontServices.framework/libFontParser.dylib +0x000000018dc3a000 /System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard +0x00000001a0166000 /System/Library/PrivateFrameworks/BoardServices.framework/Versions/A/BoardServices +0x00000001a2035000 /System/Library/PrivateFrameworks/BackBoardServices.framework/Versions/A/BackBoardServices +0x000000023b7f0000 /System/Library/PrivateFrameworks/BackBoardHIDEventFoundation.framework/Versions/A/BackBoardHIDEventFoundation +0x00000001884f2000 /System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay +0x0000000197b71000 /System/Library/Frameworks/VideoToolbox.framework/Versions/A/VideoToolbox +0x0000000195bad000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders +0x0000000269b7f000 /System/Library/PrivateFrameworks/ProDisplayLibrary.framework/Versions/A/ProDisplayLibrary +0x00000001a5e6a000 /System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/Versions/A/IOSurfaceAccelerator +0x0000000192124000 /System/Library/Frameworks/Metal.framework/Versions/A/Metal +0x0000000192119000 /System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator +0x0000000192428000 /System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia +0x000000018c27d000 /System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC +0x0000000197b29000 /System/Library/PrivateFrameworks/WatchdogClient.framework/Versions/A/WatchdogClient +0x000000018fb9f000 /System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore +0x0000000197b2b000 /System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport +0x00000001cb36c000 /usr/lib/swift/libswiftAccelerate.dylib +0x00000001b34a8000 /usr/lib/swift/libswiftCoreAudio.dylib +0x00000001cf4fd000 /usr/lib/swift/libswiftCoreMedia.dylib +0x00000001c149e000 /usr/lib/swift/libswiftMetal.dylib +0x00000001d0cb0000 /usr/lib/swift/libswiftOSLog.dylib +0x00000001c68c4000 /usr/lib/swift/libswiftQuartzCore.dylib +0x00000001cb35c000 /usr/lib/swift/libswiftUniformTypeIdentifiers.dylib +0x000000028b1a6000 /usr/lib/swift/libswiftVideoToolbox.dylib +0x00000001b7022000 /usr/lib/swift/libswiftsimd.dylib +0x00000001c881f000 /System/Library/PrivateFrameworks/BiomeStorage.framework/Versions/A/BiomeStorage +0x0000000258030000 /System/Library/PrivateFrameworks/IntelligencePlatformLibrary.framework/Versions/A/IntelligencePlatformLibrary +0x0000000268943000 /System/Library/PrivateFrameworks/PoirotSchematizer.framework/Versions/A/PoirotSchematizer +0x000000023c1a6000 /System/Library/PrivateFrameworks/BiomeSync.framework/Versions/A/BiomeSync +0x000000023b8d1000 /System/Library/PrivateFrameworks/BiomeDSL.framework/Versions/A/BiomeDSL +0x00000001e1979000 /System/Library/PrivateFrameworks/FeatureFlags.framework/Versions/A/FeatureFlags +0x00000002689b1000 /System/Library/PrivateFrameworks/PoirotUDFs.framework/Versions/A/PoirotUDFs +0x000000028b24e000 /usr/lib/swift/libswift_DarwinFoundation2.dylib +0x000000028b24f000 /usr/lib/swift/libswift_DarwinFoundation3.dylib +0x00000001a06db000 /System/Library/PrivateFrameworks/CoreTime.framework/Versions/A/CoreTime +0x0000000195944000 /usr/lib/libiconv.2.dylib +0x00000001958a1000 /usr/lib/libcharset.1.dylib +0x0000000268906000 /System/Library/PrivateFrameworks/PoirotSQLite.framework/Versions/A/PoirotSQLite +0x000000028b250000 /usr/lib/swift/libswift_RegexParser.dylib +0x000000023d7dc000 /System/Library/PrivateFrameworks/CascadeSets.framework/Versions/A/CascadeSets +0x000000019a114000 /System/Library/PrivateFrameworks/CorePhoneNumbers.framework/Versions/A/CorePhoneNumbers +0x0000000197923000 /System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG +0x00000001972fc000 /usr/lib/libexpat.1.dylib +0x00000001980ee000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib +0x0000000198119000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib +0x0000000198201000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib +0x0000000197968000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib +0x000000019700c000 /usr/lib/libate.dylib +0x00000001981a8000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib +0x000000019819f000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib +0x000000024dc80000 /System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/32023/Libraries/libllvm-flatbuffers.dylib +0x00000002486df000 /System/Library/PrivateFrameworks/FramePacing.framework/Versions/A/FramePacing +0x000000022b7ef000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib +0x000000024980d000 /System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/32023/Libraries/libGPUCompilerUtils.dylib +0x00000001a0233000 /System/Library/PrivateFrameworks/GraphicsServices.framework/Versions/A/GraphicsServices +0x000000022b7fd000 /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL +0x000000022b84e000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib +0x000000022b811000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib +0x000000022b9de000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib +0x000000022b81a000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib +0x000000022b80e000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib +0x000000022b7f7000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib +0x000000019819a000 /System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler +0x000000019817a000 /System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment +0x00000001981a2000 /System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay +0x000000027f205000 /System/Library/PrivateFrameworks/VideoToolboxParavirtualizationSupport.framework/Versions/A/VideoToolboxParavirtualizationSupport +0x00000001972b3000 /System/Library/PrivateFrameworks/AppleVA.framework/Versions/A/AppleVA +0x000000022d87a000 /System/Library/Frameworks/ExtensionFoundation.framework/Versions/A/ExtensionFoundation +0x0000000198207000 /System/Library/PrivateFrameworks/CMCaptureCore.framework/Versions/A/CMCaptureCore +0x000000019758d000 /usr/lib/libspindump.dylib +0x0000000188c00000 /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio +0x0000000197580000 /System/Library/PrivateFrameworks/AppServerSupport.framework/Versions/A/AppServerSupport +0x0000000199f0c000 /System/Library/PrivateFrameworks/perfdata.framework/Versions/A/perfdata +0x0000000188613000 /System/Library/PrivateFrameworks/AudioToolboxCore.framework/Versions/A/AudioToolboxCore +0x00000001923fe000 /System/Library/PrivateFrameworks/caulk.framework/Versions/A/caulk +0x0000000199b02000 /usr/lib/libAudioStatistics.dylib +0x00000001b24a3000 /System/Library/PrivateFrameworks/SystemPolicy.framework/Versions/A/SystemPolicy +0x0000000199db0000 /usr/lib/libSMC.dylib +0x00000001b9e19000 /usr/lib/swift/libswiftCoreMIDI.dylib +0x00000001a5159000 /System/Library/Frameworks/CoreMIDI.framework/Versions/A/CoreMIDI +0x00000001980c8000 /usr/lib/libAudioToolboxUtility.dylib +0x0000000199f1a000 /usr/lib/libperfcheck.dylib +0x000000023b18a000 /System/Library/PrivateFrameworks/AudioAnalytics.framework/Versions/A/AudioAnalytics +0x00000001d945a000 /System/Library/Frameworks/OSLog.framework/Versions/A/OSLog +0x00000002643b3000 /System/Library/PrivateFrameworks/OSEligibility.framework/Versions/A/OSEligibility +0x0000000197382000 /System/Library/PrivateFrameworks/IconServices.framework/Versions/A/IconServices +0x000000022ec61000 /System/Library/Frameworks/LightweightCodeRequirements.framework/Versions/A/LightweightCodeRequirements +0x00000001971fc000 /System/Library/PrivateFrameworks/PlugInKit.framework/Versions/A/PlugInKit +0x0000000194a56000 /System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices +0x0000000197321000 /System/Library/PrivateFrameworks/IconFoundation.framework/Versions/A/IconFoundation +0x0000000254bbc000 /System/Library/PrivateFrameworks/IconRendering.framework/Versions/A/IconRendering +0x0000000190006000 /System/Library/PrivateFrameworks/CoreUI.framework/Versions/A/CoreUI +0x0000000192f30000 /System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage +0x000000026bdae000 /System/Library/PrivateFrameworks/SFSymbols.framework/Versions/A/SFSymbols +0x000000022d730000 /System/Library/Frameworks/DeveloperToolsSupport.framework/Versions/A/DeveloperToolsSupport +0x00000001aa406000 /System/Library/PrivateFrameworks/RenderBox.framework/Versions/A/RenderBox +0x0000000192bb1000 /System/Library/PrivateFrameworks/CoreSVG.framework/Versions/A/CoreSVG +0x000000019828b000 /System/Library/PrivateFrameworks/TextureIO.framework/Versions/A/TextureIO +0x00000001b3528000 /usr/lib/swift/libswiftCoreImage.dylib +0x0000000197530000 /System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer +0x00000002485ea000 /System/Library/PrivateFrameworks/FontServices.framework/Versions/A/FontServices +0x0000000197540000 /System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG +0x000000018ffb5000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib +0x000000028a39f000 /usr/lib/libhvf.dylib +0x0000000265040000 /System/Library/PrivateFrameworks/ParsingInternal.framework/Versions/A/ParsingInternal +0x00000002485ee000 /System/Library/PrivateFrameworks/FontServices.framework/libXTFontStaticRegistryData.dylib +0x000000019341b000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSCore.framework/Versions/A/MPSCore +0x0000000195226000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSImage.framework/Versions/A/MPSImage +0x0000000194be5000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork +0x0000000195024000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix +0x0000000194e3c000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector +0x0000000195056000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNDArray.framework/Versions/A/MPSNDArray +0x000000022fae6000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSFunctions.framework/Versions/A/MPSFunctions +0x000000022fac7000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSBenchmarkLoop.framework/Versions/A/MPSBenchmarkLoop +0x000000022fafa000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSHost.framework/Versions/A/MPSHost +0x0000000186369000 /System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools +0x00000001b8749000 /System/Library/PrivateFrameworks/IOAccelMemoryInfo.framework/Versions/A/IOAccelMemoryInfo +0x00000001c6cb7000 /System/Library/PrivateFrameworks/kperf.framework/Versions/A/kperf +0x00000001b34a4000 /System/Library/PrivateFrameworks/GPURawCounter.framework/Versions/A/GPURawCounter +0x00000001a3ed5000 /System/Library/PrivateFrameworks/ASEProcessing.framework/Versions/A/ASEProcessing +0x00000001d4dc6000 /System/Library/PrivateFrameworks/Symbolication.framework/Versions/A/Symbolication +0x00000002684f1000 /System/Library/PrivateFrameworks/PhotosensitivityProcessing.framework/Versions/A/PhotosensitivityProcessing +0x000000026be34000 /System/Library/PrivateFrameworks/SILManager.framework/Versions/A/SILManager +0x00000001a057f000 /System/Library/PrivateFrameworks/CoreSymbolication.framework/Versions/A/CoreSymbolication +0x00000001b3417000 /System/Library/PrivateFrameworks/MallocStackLogging.framework/Versions/A/MallocStackLogging +0x00000001a055d000 /System/Library/PrivateFrameworks/DebugSymbols.framework/Versions/A/DebugSymbols +0x00000001c5408000 /System/Library/PrivateFrameworks/OSAnalytics.framework/Versions/A/OSAnalytics +0x0000000246345000 /System/Library/PrivateFrameworks/DeviceRecovery.framework/Versions/A/DeviceRecovery +0x000000027aa8d000 /System/Library/PrivateFrameworks/Tightbeam.framework/Versions/A/Tightbeam +0x00000001c14ac000 /usr/lib/swift/libswiftCompression.dylib +0x00000001cbaf9000 /System/Library/PrivateFrameworks/AFKUser.framework/Versions/A/AFKUser +0x00000001981d3000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATSUI.framework/Versions/A/ATSUI +0x00000001998ab000 /System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox +0x0000000195698000 /System/Library/Frameworks/UserNotifications.framework/Versions/A/UserNotifications +0x00000001b8ee0000 /System/Library/PrivateFrameworks/SiriInstrumentation.framework/Versions/A/SiriInstrumentation +0x000000026e2c4000 /System/Library/PrivateFrameworks/SiriAnalytics.framework/Versions/A/SiriAnalytics +0x00000001b663f000 /System/Library/PrivateFrameworks/FeedbackLogger.framework/Versions/A/FeedbackLogger +0x00000001d0f49000 /usr/lib/swift/libswiftAVFoundation.dylib +0x000000027d2b6000 /System/Library/PrivateFrameworks/UnifiedAssetFramework.framework/Versions/A/UnifiedAssetFramework +0x0000000199a81000 /System/Library/PrivateFrameworks/AudioSession.framework/Versions/A/AudioSession +0x0000000197441000 /System/Library/PrivateFrameworks/MediaExperience.framework/Versions/A/MediaExperience +0x0000000199855000 /System/Library/PrivateFrameworks/AudioSession.framework/libSessionUtility.dylib +0x000000019f109000 /System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth +0x00000001948c9000 /System/Library/PrivateFrameworks/CoreUtils.framework/Versions/A/CoreUtils +0x00000001ab13a000 /System/Library/PrivateFrameworks/HID.framework/Versions/A/HID +0x0000000245236000 /System/Library/PrivateFrameworks/CoreUtilsExtras.framework/Versions/A/CoreUtilsExtras +0x0000000254b11000 /System/Library/PrivateFrameworks/IO80211.framework/Versions/A/IO80211 +0x000000019ba4c000 /System/Library/Frameworks/IOBluetooth.framework/Versions/A/IOBluetooth +0x000000028b05b000 /usr/lib/swift/libswiftRegexBuilder.dylib +0x000000019709c000 /usr/lib/libIOReport.dylib +0x00000001e19fe000 /System/Library/PrivateFrameworks/WiFiPeerToPeer.framework/Versions/A/WiFiPeerToPeer +0x0000000194a65000 /System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation +0x000000023d890000 /System/Library/PrivateFrameworks/Centauri.framework/Versions/A/Centauri +0x0000000186d4d000 /System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon +0x000000028a7ef000 /usr/lib/libmrc.dylib +0x0000000254b7c000 /System/Library/PrivateFrameworks/IPConfiguration.framework/Versions/A/IPConfiguration +0x00000001d559d000 /System/Library/PrivateFrameworks/Netrb.framework/Versions/A/Netrb +0x00000001a008c000 /System/Library/PrivateFrameworks/FrontBoardServices.framework/Versions/A/FrontBoardServices +0x0000000194bce000 /usr/lib/libgermantok.dylib +0x000000019360a000 /System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData +0x000000019f312000 /System/Library/PrivateFrameworks/MediaKit.framework/Versions/A/MediaKit +0x000000019f260000 /System/Library/Frameworks/DiscRecording.framework/Versions/A/DiscRecording +0x0000000197317000 /usr/lib/libheimdal-asn1.dylib +0x00000001a2d3d000 /System/Library/Frameworks/AudioUnit.framework/Versions/A/AudioUnit +0x00000001902cc000 /System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory +0x00000001902da000 /System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory +0x000000019bdf4000 /System/Library/PrivateFrameworks/GeoServices.framework/Versions/A/GeoServices +0x000000019981b000 /System/Library/PrivateFrameworks/LocationSupport.framework/Versions/A/LocationSupport +0x000000025183a000 /System/Library/PrivateFrameworks/GeoServicesCore.framework/Versions/A/GeoServicesCore +0x00000001ac03e000 /System/Library/PrivateFrameworks/PhoneNumbers.framework/Versions/A/PhoneNumbers +0x0000000259fc2000 /System/Library/PrivateFrameworks/LocationLogEncryption.framework/Versions/A/LocationLogEncryption +0x000000022bd6a000 /System/Library/Frameworks/AVFAudio.framework/Versions/A/AVFAudio +0x000000022beae000 /System/Library/Frameworks/AVRouting.framework/Versions/A/AVRouting +0x00000001ac156000 /usr/lib/libAccessibility.dylib +0x00000002589ac000 /System/Library/PrivateFrameworks/IsolatedCoreAudioClient.framework/Versions/A/IsolatedCoreAudioClient +0x000000024102a000 /System/Library/PrivateFrameworks/CoreAudioOrchestration.framework/Versions/A/CoreAudioOrchestration +0x00000001986ef000 /System/Library/Frameworks/MediaToolbox.framework/Versions/A/MediaToolbox +0x000000019f438000 /System/Library/PrivateFrameworks/CoreAVCHD.framework/Versions/A/CoreAVCHD +0x000000019e35c000 /System/Library/Frameworks/MediaAccessibility.framework/Versions/A/MediaAccessibility +0x000000019f434000 /System/Library/PrivateFrameworks/Mangrove.framework/Versions/A/Mangrove +0x000000023cecc000 /System/Library/PrivateFrameworks/CMPhoto.framework/Versions/A/CMPhoto +0x000000019fc01000 /System/Library/Frameworks/CoreTelephony.framework/Versions/A/CoreTelephony +0x000000019f427000 /System/Library/PrivateFrameworks/CoreAUC.framework/Versions/A/CoreAUC +0x000000023a032000 /System/Library/PrivateFrameworks/AppleJPEGXL.framework/Versions/A/AppleJPEGXL +0x000000019a124000 /usr/lib/libTelephonyUtilDynamic.dylib +0x00000001dc57b000 /System/Library/Frameworks/CryptoKit.framework/Versions/A/CryptoKit +0x00000001a2d38000 /System/Library/PrivateFrameworks/CryptoKitCBridging.framework/Versions/A/CryptoKitCBridging +0x00000001a0245000 /System/Library/Frameworks/CryptoTokenKit.framework/Versions/A/CryptoTokenKit +0x0000000245db5000 /System/Library/PrivateFrameworks/Dendrite.framework/Versions/A/Dendrite +0x00000001b3048000 /System/Library/Frameworks/NaturalLanguage.framework/Versions/A/NaturalLanguage +0x000000025153d000 /System/Library/PrivateFrameworks/GenerativeModels.framework/Versions/A/GenerativeModels +0x00000001e1985000 /usr/lib/swift/libswiftNaturalLanguage.dylib +0x000000023aba9000 /System/Library/PrivateFrameworks/AppleMobileFileIntegrity.framework/Versions/A/AppleMobileFileIntegrity +0x000000028993d000 /usr/lib/libTLE.dylib +0x00000001b3449000 /usr/lib/libmis.dylib +0x00000001eb0cd000 /System/Library/PrivateFrameworks/ConfigProfileHelper.framework/Versions/A/ConfigProfileHelper +0x00000001a2ec7000 /System/Library/PrivateFrameworks/Espresso.framework/Versions/A/Espresso +0x0000000190a5c000 /System/Library/Frameworks/CoreML.framework/Versions/A/CoreML +0x00000001df831000 /usr/lib/libedit.3.dylib +0x0000000228b78000 /System/Library/PrivateFrameworks/ANECompiler.framework/Versions/A/ANECompiler +0x00000001a4f9d000 /System/Library/PrivateFrameworks/AppleNeuralEngine.framework/Versions/A/AppleNeuralEngine +0x000000025a0e0000 /System/Library/PrivateFrameworks/MIL.framework/Versions/A/MIL +0x000000022fb00000 /System/Library/Frameworks/MetalPerformanceShadersGraph.framework/Versions/A/MetalPerformanceShadersGraph +0x000000025a84f000 /System/Library/PrivateFrameworks/MLCompilerServices.framework/Versions/A/MLCompilerServices +0x00000001a3d19000 /System/Library/PrivateFrameworks/ANEServices.framework/Versions/A/ANEServices +0x00000001b870c000 /usr/lib/libncurses.5.4.dylib +0x0000000189f0a000 /usr/lib/libsandbox.1.dylib +0x000000019723d000 /usr/lib/libMatch.1.dylib +0x0000000264135000 /System/Library/PrivateFrameworks/ODIE.framework/Versions/A/ODIE +0x000000025d4dc000 /System/Library/PrivateFrameworks/MLModelAsset.framework/Versions/A/MLModelAsset +0x000000025a7f5000 /System/Library/PrivateFrameworks/MLCompilerRuntime.framework/Versions/A/MLCompilerRuntime +0x0000000194e91000 /System/Library/Frameworks/MLCompute.framework/Versions/A/MLCompute +0x000000025a777000 /System/Library/PrivateFrameworks/MLAssetIO.framework/Versions/A/MLAssetIO +0x000000028b02d000 /usr/lib/swift/libswiftMLCompute.dylib +0x000000019fe1c000 /System/Library/PrivateFrameworks/AVFCore.framework/Versions/A/AVFCore +0x00000001a9a57000 /System/Library/PrivateFrameworks/AVFCapture.framework/Versions/A/AVFCapture +0x000000023ccc3000 /System/Library/PrivateFrameworks/CMImaging.framework/Versions/A/CMImaging +0x00000001a9c99000 /System/Library/PrivateFrameworks/Quagga.framework/Versions/A/Quagga +0x00000001a9dca000 /System/Library/PrivateFrameworks/CMCapture.framework/Versions/A/CMCapture +0x0000000199cad000 /System/Library/Frameworks/CoreMediaIO.framework/Versions/A/CoreMediaIO +0x000000023cbfe000 /System/Library/PrivateFrameworks/CMCaptureDevice.framework/Versions/A/CMCaptureDevice +0x0000000197679000 /System/Library/PrivateFrameworks/CoreBrightness.framework/Versions/A/CoreBrightness +0x000000023dd87000 /System/Library/PrivateFrameworks/CinematicFraming.framework/Versions/A/CinematicFraming +0x0000000260619000 /System/Library/PrivateFrameworks/ModelManagerServices.framework/Versions/A/ModelManagerServices +0x00000001cdfb1000 /System/Library/PrivateFrameworks/CPMS.framework/Versions/A/CPMS +0x00000002781bd000 /System/Library/PrivateFrameworks/SystemStatus.framework/Versions/A/SystemStatus +0x00000001b1e78000 /System/Library/Frameworks/CoreMotion.framework/Versions/A/CoreMotion +0x00000001c2490000 /System/Library/PrivateFrameworks/TimeSync.framework/Versions/A/TimeSync +0x000000024677d000 /System/Library/PrivateFrameworks/DistributedSensing.framework/Versions/A/DistributedSensing +0x00000001bd86f000 /System/Library/PrivateFrameworks/MobileBluetooth.framework/Versions/A/MobileBluetooth +0x00000001c4154000 /System/Library/PrivateFrameworks/IOKitten.framework/Versions/A/IOKitten +0x0000000239eac000 /System/Library/PrivateFrameworks/AppleIntelligenceReporting.framework/Versions/A/AppleIntelligenceReporting +0x00000001947d8000 /System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji +0x0000000187061000 /usr/lib/libCRFSuite.dylib +0x0000000188342000 /System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling +0x00000001934ea000 /System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP +0x000000018d2bf000 /System/Library/PrivateFrameworks/Montreal.framework/Versions/A/Montreal +0x000000019594c000 /usr/lib/libcmph.dylib +0x0000000194b6f000 /usr/lib/libmecab.dylib +0x0000000195abd000 /usr/lib/libThaiTokenizer.dylib +0x000000025161f000 /System/Library/PrivateFrameworks/GenerativeModelsFoundation.framework/Versions/A/GenerativeModelsFoundation +0x000000027af92000 /System/Library/PrivateFrameworks/TokenGeneration.framework/Versions/A/TokenGeneration +0x00000002513f3000 /System/Library/PrivateFrameworks/GenerativeFunctions.framework/Versions/A/GenerativeFunctions +0x0000000251441000 /System/Library/PrivateFrameworks/GenerativeFunctionsFoundation.framework/Versions/A/GenerativeFunctionsFoundation +0x00000002602da000 /System/Library/PrivateFrameworks/ModelCatalog.framework/Versions/A/ModelCatalog +0x000000026d4de000 /System/Library/PrivateFrameworks/SensitiveContentAnalysisML.framework/Versions/A/SensitiveContentAnalysisML +0x00000002514d7000 /System/Library/PrivateFrameworks/GenerativeFunctionsInstrumentation.framework/Versions/A/GenerativeFunctionsInstrumentation +0x000000026a307000 /System/Library/PrivateFrameworks/PromptKit.framework/Versions/A/PromptKit +0x0000000269d1d000 /System/Library/PrivateFrameworks/ProactiveDaemonSupport.framework/Versions/A/ProactiveDaemonSupport +0x000000027b1ca000 /System/Library/PrivateFrameworks/TokenGenerationCore.framework/Versions/A/TokenGenerationCore +0x00000001b3f17000 /System/Library/PrivateFrameworks/Trial.framework/Versions/A/Trial +0x00000001b3e98000 /System/Library/PrivateFrameworks/TrialProto.framework/Versions/A/TrialProto +0x0000000239ad6000 /System/Library/PrivateFrameworks/AppleFlatBuffers.framework/Versions/A/AppleFlatBuffers +0x000000026d7ae000 /System/Library/PrivateFrameworks/SentencePieceInternal.framework/Versions/A/SentencePieceInternal +0x000000019e3b6000 /System/Library/Frameworks/Vision.framework/Versions/A/Vision +0x0000000244ed4000 /System/Library/PrivateFrameworks/CoreSceneUnderstanding.framework/Versions/A/CoreSceneUnderstanding +0x000000027fe1b000 /System/Library/PrivateFrameworks/VisionCore.framework/Versions/A/VisionCore +0x000000019862c000 /System/Library/PrivateFrameworks/DataDetectorsCore.framework/Versions/A/DataDetectorsCore +0x00000001bc7fd000 /System/Library/Frameworks/Vision.framework/libfaceCore.dylib +0x00000001bd317000 /System/Library/PrivateFrameworks/Futhark.framework/Versions/A/Futhark +0x00000001c1265000 /System/Library/PrivateFrameworks/InertiaCam.framework/Versions/A/InertiaCam +0x00000001bd0a4000 /System/Library/PrivateFrameworks/TextRecognition.framework/Versions/A/TextRecognition +0x000000022d719000 /System/Library/Frameworks/DataDetection.framework/Versions/A/DataDetection +0x00000001b72b0000 /System/Library/PrivateFrameworks/TextInput.framework/Versions/A/TextInput +0x00000001970da000 /System/Library/PrivateFrameworks/CVNLP.framework/Versions/A/CVNLP +0x00000001d9945000 /System/Library/PrivateFrameworks/HIDDisplay.framework/Versions/A/HIDDisplay +0x0000000199e91000 /usr/lib/libcups.2.dylib +0x0000000199f28000 /System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos +0x0000000199b98000 /usr/lib/libresolv.9.dylib +0x0000000197594000 /System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal +0x00000001a2d3c000 /System/Library/Frameworks/Kerberos.framework/Versions/A/Libraries/libHeimdalProxy.dylib +0x0000000199f8c000 /System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth +0x00000001ac04a000 /System/Library/PrivateFrameworks/AXCoreUtilities.framework/Versions/A/AXCoreUtilities +0x00000001bc646000 /System/Library/PrivateFrameworks/AttributeGraph.framework/Versions/A/AttributeGraph +0x000000028943d000 /usr/lib/libAXSafeCategoryBundle.dylib +0x0000000233e8e000 /System/Library/Frameworks/TabularData.framework/Versions/A/TabularData +0x000000023ad59000 /System/Library/PrivateFrameworks/ArgumentParserInternal.framework/Versions/A/ArgumentParserInternal +0x000000019469a000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib +0x0000000195c8f000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib +0x0000000194bd1000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib +0x0000000195b03000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib +0x0000000195c8a000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib +0x0000000193611000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib +0x0000000186e5d000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib +0x000000022d1fe000 /System/Library/Frameworks/CoreTransferable.framework/Versions/A/CoreTransferable +0x0000000199ef0000 /System/Library/PrivateFrameworks/NetAuth.framework/Versions/A/NetAuth +0x00000001904f0000 /System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport +0x000000018b68d000 /System/Library/PrivateFrameworks/UIFoundation.framework/Versions/A/UIFoundation +0x0000000194b39000 /usr/lib/libCheckFix.dylib +0x000000018f677000 /System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities +0x0000000255603000 /System/Library/PrivateFrameworks/InstalledContentLibrary.framework/Versions/A/InstalledContentLibrary +0x0000000189dce000 /System/Library/PrivateFrameworks/CoreServicesStore.framework/Versions/A/CoreServicesStore +0x000000019033b000 /usr/lib/libapp_launch_measurement.dylib +0x00000001c6dce000 /System/Library/PrivateFrameworks/MobileSystemServices.framework/Versions/A/MobileSystemServices +0x0000000196f5f000 /usr/lib/libxslt.1.dylib +0x0000000194af8000 /System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement +0x00000001a23ce000 /usr/lib/libcurl.4.dylib +0x000000028a153000 /usr/lib/libcrypto.46.dylib +0x000000028acd6000 /usr/lib/libssl.48.dylib +0x00000001a20a8000 /System/Library/Frameworks/LDAP.framework/Versions/A/LDAP +0x00000001a20e4000 /System/Library/PrivateFrameworks/TrustEvaluationAgent.framework/Versions/A/TrustEvaluationAgent +0x0000000199bb5000 /usr/lib/libsasl2.2.dylib +0x00000001a534c000 /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa +0x0000000189f69000 /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit +0x000000023ec0a000 /System/Library/PrivateFrameworks/CollectionViewCore.framework/Versions/A/CollectionViewCore +0x0000000192bab000 /System/Library/PrivateFrameworks/XCTTargetBootstrap.framework/Versions/A/XCTTargetBootstrap +0x000000019867e000 /System/Library/PrivateFrameworks/UserActivity.framework/Versions/A/UserActivity +0x00000002486ec000 /System/Library/PrivateFrameworks/FrontBoard.framework/Versions/A/FrontBoard +0x000000027cbdb000 /System/Library/PrivateFrameworks/UIIntelligenceSupport.framework/Versions/A/UIIntelligenceSupport +0x0000000232f17000 /System/Library/Frameworks/SwiftUICore.framework/Versions/A/SwiftUICore +0x0000000283787000 /System/Library/PrivateFrameworks/WritingTools.framework/Versions/A/WritingTools +0x000000028257e000 /System/Library/PrivateFrameworks/WindowManagement.framework/Versions/A/WindowManagement +0x000000024841c000 /System/Library/PrivateFrameworks/FocusEngine.framework/Versions/A/FocusEngine +0x0000000245e25000 /System/Library/PrivateFrameworks/DesignLibrary.framework/Versions/A/DesignLibrary +0x0000000192b96000 /System/Library/PrivateFrameworks/DFRFoundation.framework/Versions/A/DFRFoundation +0x000000027db28000 /System/Library/PrivateFrameworks/UpdateCycle.framework/Versions/A/UpdateCycle +0x000000019289a000 /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox +0x000000019dc7c000 /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/SpeechRecognition.framework/Versions/A/SpeechRecognition +0x00000001902c2000 /System/Library/PrivateFrameworks/PerformanceAnalysis.framework/Versions/A/PerformanceAnalysis +0x000000019e00c000 /System/Library/Frameworks/Accessibility.framework/Versions/A/Accessibility +0x0000000233e74000 /System/Library/Frameworks/Symbols.framework/Versions/A/Symbols +0x00000002519e8000 /System/Library/PrivateFrameworks/Gestures.framework/Versions/A/Gestures +0x000000028b0fe000 /usr/lib/swift/libswiftSpatial.dylib +0x00000001b028a000 /usr/lib/swift/libswiftCoreGraphics.dylib +0x000000019ea50000 /usr/lib/swift/libswiftFoundation.dylib +0x00000001eaa6e000 /usr/lib/swift/libswiftSwiftOnoneSupport.dylib +0x000000028b384000 /usr/lib/swift/libswiftsys_time.dylib +0x00000001d55db000 /System/Library/PrivateFrameworks/CoreMaterial.framework/Versions/A/CoreMaterial +0x000000028993a000 /usr/lib/libSpatial.dylib +0x000000028935a000 /System/Library/SubFrameworks/UIUtilities.framework/Versions/A/UIUtilities +0x0000000105b3c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/server/libjvm.dylib +0x00000001048f4000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjimage.dylib +0x0000000104950000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjdwp.dylib +0x0000000104998000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjava.dylib +0x0000000104924000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libinstrument.dylib +0x0000000104a0c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +0x000000028942f000 /usr/lib/i18n/libiconv_std.dylib +0x0000000289425000 /usr/lib/i18n/libUTF8.dylib +0x0000000289434000 /usr/lib/i18n/libmapper_none.dylib +0x0000000104a98000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libdt_socket.dylib +0x0000000104b90000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnio.dylib +0x0000000104bd4000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnet.dylib +0x0000000104b70000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement.dylib +0x0000000104bb0000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement_ext.dylib +0x0000000104f00000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libextnet.dylib +0x0000000104f30000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libverify.dylib + + +VM Arguments: +jvm_args: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:57772,suspend=y,server=n -javaagent:/Users/liangxin/Library/Caches/JetBrains/IntelliJIdea2026.1/captureAgent/debugger-agent.jar=file:///var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/capture2936228657643338079.props -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Ddebugger.async.stack.trace.for.all.threads=true -Dfile.encoding=UTF-8 +java_command: org.springblade.transport.TransportApplication +java_class_path (initial): /Users/liangxin/Project/JAVA/tms-erp-api/blade-service/blade-transport/target/classes:/Users/liangxin/.m2/repository/org/springblade/blade-core-boot/4.10.0.BASE-SNAPSHOT/blade-core-boot-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-context/4.10.0.BASE-SNAPSHOT/blade-core-context-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-db/4.10.0.BASE-SNAPSHOT/blade-core-db-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-starter-jdbc/3.5.16/spring-boot-starter-jdbc-3.5.16.jar:/Users/liangxin/.m2/repository/com/zaxxer/HikariCP/6.3.3/HikariCP-6.3.3.jar:/Users/liangxin/.m2/repository/com/baomidou/mybatis-plus-spring-boot3-starter/3.5.16/mybatis-plus-spring-boot3-starter-3.5.16.jar:/Users/liangxin/.m2/repository/com/alibaba/druid-spring-boot-3-starter/1.2.28/druid-spring-boot-3-starter-1.2.28.jar:/Users/liangxin/.m2/repository/com/mysql/mysql-connector-j/9.4.0/mysql-connector-j-9.4.0.jar:/Users/liangxin/.m2/repository/com/google/protobuf/protobuf-java/4.31.1/protobuf-java-4.31.1.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-secure/4.10.0.BASE-SNAPSHOT/blade-core-secure-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-cloud/4.10.0.BASE-SNAPSHOT/blade-core-cloud-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/de/codecentric/spring-boot-admin-starter-client/3.5.9/spring-boot-admin-starter-client-3.5.9.jar:/Users/liangxin/.m2/repository/de/codecentric/spring-boot-admin-client/3.5.9/spring-boot-admin-client-3.5.9.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-starter-actuator/3.5.16/spring-boot-starter-actuator-3.5.16.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-actuator-autoconfigure/3.5.16/spring-boot-actuator-autoconfigure-3.5.16.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-actuator/3.5.16/spring-boot-act +Launcher Type: SUN_STANDARD + +[Global flags] + intx CICompilerCount = 4 {product} {ergonomic} + uint ConcGCThreads = 3 {product} {ergonomic} + uint G1ConcRefinementThreads = 10 {product} {ergonomic} + size_t G1HeapRegionSize = 8388608 {product} {ergonomic} + uintx GCDrainStackTargetSize = 64 {product} {ergonomic} + size_t InitialHeapSize = 603979776 {product} {ergonomic} + bool ManagementServer = true {product} {command line} + size_t MarkStackSize = 4194304 {product} {ergonomic} + size_t MaxHeapSize = 9663676416 {product} {ergonomic} + size_t MaxNewSize = 5796528128 {product} {ergonomic} + size_t MinHeapDeltaBytes = 8388608 {product} {ergonomic} + size_t MinHeapSize = 8388608 {product} {ergonomic} + uintx NonProfiledCodeHeapSize = 0 {pd product} {ergonomic} + bool ProfileInterpreter = false {pd product} {command line} + uintx ProfiledCodeHeapSize = 0 {pd product} {ergonomic} + size_t SoftMaxHeapSize = 9663676416 {manageable} {ergonomic} + intx TieredStopAtLevel = 1 {product} {command line} + bool UseCompressedClassPointers = true {product lp64_product} {ergonomic} + bool UseCompressedOops = true {product lp64_product} {ergonomic} + bool UseG1GC = true {product} {ergonomic} + bool UseNUMA = false {product} {ergonomic} + bool UseNUMAInterleaving = false {product} {ergonomic} + +Logging: +Log output configuration: + #0: stdout all=warning uptime,level,tags + #1: stderr all=off uptime,level,tags + +Environment Variables: +JAVA_HOME=/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home +PATH=/Users/liangxin/ai-infra/.venv/bin:/Users/liangxin/.nacos/bin:/Applications/Docker.app/Contents/Resources/bin:/Users/liangxin/Library/pnpm:/opt/homebrew/opt/ruby@3.2/bin:/opt/homebrew/opt/openssl@3/bin:/opt/miniconda3/bin:/opt/miniconda3/condabin:/usr/local/sbin:/usr/local/bin:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Users/liangxin/fvm/default/bin:/Applications/MAMP/bin/php/php8.1.13/bin:/opt/homebrew/opt/ruby@3.2/bin:/Users/liangxin/.nvm/versions/node/v20.18.3/bin:/Applications/apache-tomcat-9.0.78:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Users/liangxin/fvm/default/bin:/opt/homebrew/opt/libpng/bin:/Applications/pngquant:/Users/liangxin/AndroidSDK/platform-tools:/Users/liangxin/Library/Android/sdk/platform-tools:/Users/liangxin/Library/Andriod/sdk/cmdline-tools/latest/bin:/Users/liangxin/Library/Andriod/sdk:/Applications/apache-maven-3.8.1/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/local/bin:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/Users/liangxin/.local/bin:/Users/liangxin/fvm/default/bin:/Applications/MAMP/bin/php/php8.1.13/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/liangxin/.cargo/bin:true:/Applications/极空间.app/Contents/Resources/app.asar.unpacked/bin/platform-tools +SHELL=/bin/zsh +LANG=C.UTF-8 +TMPDIR=/var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/ + +Active Locale: +LC_ALL=C.UTF-8 +LC_COLLATE=C.UTF-8 +LC_CTYPE=C.UTF-8 +LC_MESSAGES=C.UTF-8 +LC_MONETARY=C.UTF-8 +LC_NUMERIC=C.UTF-8 +LC_TIME=C.UTF-8 + +Signal Handlers: + SIGSEGV: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGBUS: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGFPE: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGPIPE: javaSignalHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGXFSZ: javaSignalHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGILL: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGUSR2: SR_handler in libjvm.dylib, mask=00000000000000000000000000000000, flags=SA_RESTART|SA_SIGINFO, blocked + SIGHUP: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGINT: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGTERM: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGQUIT: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGTRAP: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + + +--------------- S Y S T E M --------------- + +OS: +uname: Darwin 25.6.0 Darwin Kernel Version 25.6.0: Fri Jul 31 19:16:36 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6030 arm64 +OS uptime: 3 days 11:55 hours +rlimit (soft/hard): STACK 8176k/65520k , CORE 0k/infinity , NPROC 6000/9000 , NOFILE 10240/65536 , AS infinity/infinity , CPU infinity/infinity , DATA infinity/infinity , FSIZE infinity/infinity , MEMLOCK infinity/infinity , RSS infinity/infinity +load average: 13.76 8.45 9.16 + +CPU: total 12 (initial active 12) 0x61:0x0:0x5f4dea93:0, fp, simd, crc, lse +machdep.cpu.brand_string:Apple M3 Pro +hw.cachelinesize:128 +hw.l1icachesize:131072 +hw.l1dcachesize:65536 +hw.l2cachesize:4194304 + +Memory: 16k page, physical 37748736k(1387216k free), swap 12582912k(791104k free) + +vm_info: OpenJDK 64-Bit Server VM (17.0.8+7-LTS) for bsd-aarch64 JRE (17.0.8+7-LTS) (Zulu17.44+15-CA), built on Jul 5 2023 00:50:04 by "zulu_re" with clang Apple LLVM 12.0.0 (clang-1200.0.32.28) + +END. From 70e44da78986024a38834e44ea67fec29ddc4ad1 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 14 Sep 2026 15:00:40 +0800 Subject: [PATCH 092/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E5=90=88=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/entity/ContractManage.java | 22 ++- .../transport/pojo/vo/ProjectApplyVO.java | 4 + .../transport/excel/ContractManageExcel.java | 13 ++ .../impl/ContractManageServiceImpl.java | 180 +++++++++++++++++- .../service/impl/ProjectApplyServiceImpl.java | 8 + .../ReceivablePayableDetailServiceImpl.java | 28 ++- .../wrapper/ContractManageWrapper.java | 1 + ...ontract_manage_archive_status_20260914.sql | 7 + ..._contract_manage_basic_fields_20260914.sql | 6 + .../blade_project_contract_management.sql | 5 + 10 files changed, 254 insertions(+), 20 deletions(-) create mode 100644 doc/sql/transport/blade_contract_manage_archive_status_20260914.sql create mode 100644 doc/sql/transport/blade_contract_manage_basic_fields_20260914.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java index f703836..d56fec7 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java @@ -31,6 +31,7 @@ import lombok.EqualsAndHashCode; import org.springblade.core.tenant.mp.TenantEntity; import java.io.Serial; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -123,12 +124,31 @@ public class ContractManage extends TenantEntity { @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) private Integer paymentDays; + @Schema(description = "合同金额") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal contractAmount; + + @Schema(description = "是否范本") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer templateFlag; + + @Schema(description = "原件合同编号") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private String originalContractNo; + + @Schema(description = "是否电子章") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer electronicSealFlag; + @Schema(description = "合同阶段") private String contractStage; @Schema(description = "审核状态") private String approvalStatus; + @Schema(description = "归档状态:未归档/已归档") + private String archiveStatus; + @Schema(description = "当前节点") private String currentNode; @@ -141,7 +161,7 @@ public class ContractManage extends TenantEntity { @Schema(description = "计费信息开关") private Integer billingEnabled; - @Schema(description = "费用生成模式:system系统生成,manual手动生成") + @Schema(description = "费用生成模式:system系统生成,manual账单导入生成") private String feeGenerationMode; @Schema(description = "合同主文件JSON") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java index 8ac54d1..0b6fa77 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java @@ -80,6 +80,10 @@ public class ProjectApplyVO extends ProjectApply { @Schema(description = "是否仅查询可用于临时额度申请的项目") private Boolean temporaryCreditLimitSelectable; + @TableField(exist = false) + @Schema(description = "是否仅查询可用于合同选择的项目(含临时项目与正式审批通过项目)") + private Boolean contractSelectable; + @TableField(exist = false) @Schema(description = "资金使用风险等级:high、medium、none") private String fundUseRisk; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java index 2802c14..f55fb68 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java @@ -23,6 +23,7 @@ package org.springblade.transport.excel; import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.NumberFormat; import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight; @@ -30,6 +31,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -77,6 +79,8 @@ public class ContractManageExcel implements Serializable { private String contractStage; @ExcelProperty("审核状态") private String approvalStatus; + @ExcelProperty("归档状态") + private String archiveStatus; @ExcelProperty("结算币种") private String settlementCurrency; @ExcelProperty("结算方式") @@ -87,6 +91,15 @@ public class ContractManageExcel implements Serializable { private Integer copyCount; @ExcelProperty("回款账期(天)") private Integer paymentDays; + @ExcelProperty("合同金额") + @NumberFormat("0.00") + private BigDecimal contractAmount; + @ExcelProperty("是否范本") + private Integer templateFlag; + @ExcelProperty("原件合同编号") + private String originalContractNo; + @ExcelProperty("是否电子章") + private Integer electronicSealFlag; @ExcelProperty("当前节点") private String currentNode; @ExcelProperty("当前处理人") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index 1c37acb..51dc7c5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -71,9 +71,13 @@ public class ContractManageServiceImpl extends BaseServiceImpl { + if (!(item instanceof Map map)) { + return false; + } + Object fileType = map.get("fileType"); + return fileType != null && FILE_TYPE_SEAL_ARCHIVE.equals(String.valueOf(fileType).trim()); + }); + } + + @SuppressWarnings("unchecked") + private String markContractFilesApproved(String contractFileJson) { + List files = parseJsonArray(contractFileJson); + if (files.isEmpty()) { + return TransportBusinessSupport.trimToNull(contractFileJson); + } + List> marked = new ArrayList<>(); + for (Object item : files) { + if (item instanceof Map map) { + Map next = new LinkedHashMap<>((Map) map); + next.put("approved", true); + marked.add(next); + } + } + return marked.isEmpty() ? null : JsonUtil.toJson(marked); + } + + @SuppressWarnings("unchecked") + private String mergeApprovedContractFiles(String originalJson, String requestJson) { + List originalFiles = parseJsonArray(originalJson); + List requestFiles = parseJsonArray(requestJson); + Map> originalApproved = new LinkedHashMap<>(); + for (Object item : originalFiles) { + if (!(item instanceof Map map)) { + continue; + } + Map file = new LinkedHashMap<>((Map) map); + if (isAttachmentApproved(file)) { + originalApproved.put(attachmentKey(file), file); + } + } + Set requestKeys = new HashSet<>(); + List> merged = new ArrayList<>(); + for (Object item : requestFiles) { + if (!(item instanceof Map map)) { + continue; + } + Map file = new LinkedHashMap<>((Map) map); + String key = attachmentKey(file); + requestKeys.add(key); + Map approvedOriginal = originalApproved.get(key); + if (approvedOriginal != null) { + // 已审核通过附件不允许改删,保留原记录。 + merged.add(approvedOriginal); + continue; + } + file.put("approved", true); + merged.add(file); + } + for (Map.Entry> entry : originalApproved.entrySet()) { + if (!requestKeys.contains(entry.getKey())) { + throw new ServiceException("已审核通过的合同文件不允许删除"); + } + } + return merged.isEmpty() ? null : JsonUtil.toJson(merged); + } + + private boolean isAttachmentApproved(Map file) { + Object approved = file.get("approved"); + if (Boolean.TRUE.equals(approved) || Objects.equals(approved, 1) || Objects.equals(String.valueOf(approved), "1")) { + return true; + } + return Objects.equals(String.valueOf(file.get("approvalStatus")), STATUS_APPROVED); } private void prepare(ContractManage contractManage) { @@ -490,6 +596,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl 2) { + throw new ServiceException("合同金额最多保留2位小数"); + } + validateFlag(contractManage.getTemplateFlag(), "是否范本"); + validateFlag(contractManage.getElectronicSealFlag(), "是否电子章"); + } + + private void validateFlag(Integer value, String fieldName) { + if (value != null && value != 0 && value != 1) { + throw new ServiceException(fieldName + "只能选择是或否"); + } } private void normalizeOptionalIntegerFields(ContractManage contractManage) { @@ -515,11 +641,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl wrapper + .nested(formal -> formal.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL) + .in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved")) + .or(temporary -> temporary.eq(ProjectApply::getEffectiveType, EFFECTIVE_TEMPORARY) + .ne(ProjectApply::getApprovalStatus, STATUS_VOIDED))); + } if (Func.isNotEmpty(projectApply.getFundUseRisk())) { List riskRecords = baseMapper.selectList(queryWrapper).stream() .map(record -> ProjectApplyWrapper.build().entityVO(record)) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 3e8c377..d5f0187 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -102,7 +102,7 @@ public class ReceivablePayableDetailServiceImpl private static final String SOURCE_MASTER_ORDER = "总单系统生成"; private static final String SOURCE_LOADING_ORDER = "配载单系统生成"; - private static final String SOURCE_MANUAL_GENERATION = "手动生成"; + private static final String SOURCE_MANUAL_GENERATION = "账单导入生成"; private static final String TRANSPORT_TYPE_SELF = "自运"; private static final String FEE_SOURCE_AUTO = "自动生成"; private static final String FEE_SOURCE_MANUAL = "手动录入"; @@ -900,7 +900,6 @@ public class ReceivablePayableDetailServiceImpl if (!(conditionValue instanceof Map condition) || !hasConfiguredMatchCondition(condition)) return true; return matchesLocation(condition, "origin", waybill.getDepartureAddressId(), waybill.getDepartureName(), waybill.getDepartureAddress()) && matchesLocation(condition, "destination", waybill.getArrivalAddressId(), waybill.getArrivalName(), waybill.getArrivalAddress()) - && matchesCondition(condition.get("transportMode"), waybill.getTransportType()) && matchesCargo(condition, waybill); } @@ -909,7 +908,6 @@ public class ReceivablePayableDetailServiceImpl || !isBlank(condition.get("originCode")) || !isBlank(condition.get("destination")) || !isBlank(condition.get("destinationCode")) - || !isBlank(condition.get("transportMode")) || !isBlank(condition.get("cargoType")) || !isBlank(condition.get("cargoTypeCode")) || !isBlank(condition.get("cargoTypePath")) @@ -1576,12 +1574,24 @@ public class ReceivablePayableDetailServiceImpl if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); if (intervalFlatPrice) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); if (intervalUnitPrice) { - BigDecimal rangeBase = base; - return range(ranges, rangeBase).map(r -> { - BigDecimal minimum = decimal(r.get("minimumBillingWeight")); - if (minimum.signum() <= 0) minimum = decimal(rule.get("minimumBillingWeight")); - BigDecimal effectiveBase = applyMinimum(rangeBase, element, minimum, waybill); - return decimal(r.get("unitPrice")).multiply(effectiveBase); + List> sortedRanges = ranges.stream() + .sorted(Comparator.comparing(item -> decimal(item.get("lowerLimit")))) + .toList(); + return range(sortedRanges, base).map(matchedRange -> { + boolean firstTier = !sortedRanges.isEmpty() && matchedRange == sortedRanges.get(0); + BigDecimal effectiveBase = base; + if (firstTier) { + BigDecimal minimum = decimal(matchedRange.get("minimumBillingWeight")); + if (minimum.signum() <= 0) { + minimum = decimal(rule.get("minimumBillingWeight")); + } + effectiveBase = applyMinimum(base, element, minimum, waybill); + } + BigDecimal matchedUnitPrice = decimal(matchedRange.get("unitPrice")); + if (matchedUnitPrice.signum() == 0) { + matchedUnitPrice = unit; + } + return matchedUnitPrice.multiply(effectiveBase); }).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); } if ("阶梯单价".equals(type)) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ContractManageWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ContractManageWrapper.java index 6d03a37..b4571b9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ContractManageWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ContractManageWrapper.java @@ -71,6 +71,7 @@ public class ContractManageWrapper extends BaseEntityWrapper "草稿"; case "reviewing" -> "审批中"; case "rejected" -> "已驳回"; + case "withdrawn" -> "已撤回"; case "approved" -> "审批通过"; case "change_reviewing" -> "变更审批中"; case "change_rejected" -> "变更驳回"; diff --git a/doc/sql/transport/blade_contract_manage_archive_status_20260914.sql b/doc/sql/transport/blade_contract_manage_archive_status_20260914.sql new file mode 100644 index 0000000..28fc3d0 --- /dev/null +++ b/doc/sql/transport/blade_contract_manage_archive_status_20260914.sql @@ -0,0 +1,7 @@ +-- 合同管理新增归档状态:默认未归档;审核通过且合同文件含双章归档文件时自动置为已归档 +ALTER TABLE `blade_contract_manage` + ADD COLUMN `archive_status` varchar(50) DEFAULT '未归档' COMMENT '归档状态:未归档/已归档' AFTER `approval_status`; + +UPDATE `blade_contract_manage` +SET `archive_status` = '未归档' +WHERE `archive_status` IS NULL OR `archive_status` = ''; diff --git a/doc/sql/transport/blade_contract_manage_basic_fields_20260914.sql b/doc/sql/transport/blade_contract_manage_basic_fields_20260914.sql new file mode 100644 index 0000000..762ab4a --- /dev/null +++ b/doc/sql/transport/blade_contract_manage_basic_fields_20260914.sql @@ -0,0 +1,6 @@ +-- 合同管理新增合同金额、范本、原件编号及电子章字段 +ALTER TABLE `blade_contract_manage` + ADD COLUMN `contract_amount` decimal(18,2) DEFAULT NULL COMMENT '合同金额' AFTER `payment_days`, + ADD COLUMN `template_flag` int(11) DEFAULT '0' COMMENT '是否范本' AFTER `contract_amount`, + ADD COLUMN `original_contract_no` varchar(100) DEFAULT NULL COMMENT '原件合同编号' AFTER `template_flag`, + ADD COLUMN `electronic_seal_flag` int(11) DEFAULT '0' COMMENT '是否电子章' AFTER `original_contract_no`; diff --git a/doc/sql/transport/blade_project_contract_management.sql b/doc/sql/transport/blade_project_contract_management.sql index caa7ebd..2d72069 100644 --- a/doc/sql/transport/blade_project_contract_management.sql +++ b/doc/sql/transport/blade_project_contract_management.sql @@ -139,8 +139,13 @@ CREATE TABLE `blade_contract_manage` ( `settlement_currency` varchar(50) DEFAULT NULL COMMENT '结算币种', `invoice_cycle` int(11) DEFAULT NULL COMMENT '开票周期(天)', `payment_days` int(11) DEFAULT NULL COMMENT '回款账期(天)', + `contract_amount` decimal(18,2) DEFAULT NULL COMMENT '合同金额', + `template_flag` int(11) DEFAULT '0' COMMENT '是否范本', + `original_contract_no` varchar(100) DEFAULT NULL COMMENT '原件合同编号', + `electronic_seal_flag` int(11) DEFAULT '0' COMMENT '是否电子章', `contract_stage` varchar(50) DEFAULT NULL COMMENT '合同阶段', `approval_status` varchar(50) DEFAULT NULL COMMENT '审核状态', + `archive_status` varchar(50) DEFAULT '未归档' COMMENT '归档状态:未归档/已归档', `current_node` varchar(100) DEFAULT NULL COMMENT '当前节点', `current_processor` varchar(100) DEFAULT NULL COMMENT '当前处理人', `approved_time` datetime DEFAULT NULL COMMENT '审核通过时间', From 087d89b31a32f6012e50fe7ff9e1fd493bea675b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 14 Sep 2026 23:43:00 +0800 Subject: [PATCH 093/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E5=90=88=E5=90=8C?= =?UTF-8?q?=E3=80=81=E8=BF=90=E5=8A=9B=E3=80=81=E5=9F=BA=E7=A1=80=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E3=80=81=E5=AE=A2=E5=95=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/entity/CustomerArchive.java | 8 + .../pojo/entity/CustomerInvoiceContact.java | 71 ++++++++ .../pojo/entity/CustomerInvoiceInfo.java | 26 +-- .../pojo/entity/InsuranceOcrTemplate.java | 5 + .../AnnualInspectionRecordExpiryStatVO.java | 55 ++++++ .../pojo/vo/AnnualInspectionRecordVO.java | 14 ++ .../transport/pojo/vo/CustomerArchiveVO.java | 8 + .../pojo/vo/CustomerInvoiceContactVO.java | 45 +++++ .../pojo/vo/CustomerInvoiceInfoVO.java | 7 + .../service/impl/DictBizServiceImpl.java | 5 +- .../system/service/impl/DictServiceImpl.java | 5 +- .../AnnualInspectionRecordController.java | 37 ++++- .../mapper/AnnualInspectionRecordMapper.java | 12 +- .../mapper/AnnualInspectionRecordMapper.xml | 78 ++++++--- .../mapper/CustomerArchiveMapper.xml | 4 + .../mapper/CustomerInvoiceContactMapper.java | 21 +++ .../IAnnualInspectionRecordService.java | 3 + .../service/ICustomerArchiveService.java | 1 + .../AnnualInspectionRecordServiceImpl.java | 27 ++- .../impl/ContractManageServiceImpl.java | 31 +++- .../impl/CustomerArchiveServiceImpl.java | 153 +++++++++++++++-- .../impl/InsuranceOcrTemplateServiceImpl.java | 9 + .../impl/InsuranceRecordServiceImpl.java | 9 + .../impl/InvoiceApplicationServiceImpl.java | 42 ++++- .../impl/OtherExpenseRecordServiceImpl.java | 22 ++- .../ReceivablePayableDetailServiceImpl.java | 4 +- doc/sql/transport/blade_customer_archive.sql | 37 ++++- ...stomer_archive_guangxi_top100_20260914.sql | 4 + ...hive_network_freight_platform_20260914.sql | 4 + ...lade_customer_invoice_contact_20260914.sql | 63 +++++++ .../blade_insurance_ocr_template.sql | 2 + ...nce_ocr_template_vehicle_type_20260914.sql | 5 + hs_err_pid22822.log | 157 ++++++++++++++++++ 33 files changed, 881 insertions(+), 93 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceContact.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordExpiryStatVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceContactVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerInvoiceContactMapper.java create mode 100644 doc/sql/transport/blade_customer_archive_guangxi_top100_20260914.sql create mode 100644 doc/sql/transport/blade_customer_archive_network_freight_platform_20260914.sql create mode 100644 doc/sql/transport/blade_customer_invoice_contact_20260914.sql create mode 100644 doc/sql/transport/blade_insurance_ocr_template_vehicle_type_20260914.sql create mode 100644 hs_err_pid22822.log diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java index 434c164..0fc9777 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java @@ -61,6 +61,10 @@ public class CustomerArchive extends TenantEntity { @Schema(description = "客商性质") private String customerNature; + @Schema(description = "是否广西百强:0否,1是") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Integer guangxiTop100; + @Schema(description = "统一社会信用代码") private String unifiedCreditCode; @@ -101,6 +105,10 @@ public class CustomerArchive extends TenantEntity { @Schema(description = "经营范围") private String businessScope; + @Schema(description = "网络货运平台:0否,1是") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Integer networkFreightPlatform; + @Schema(description = "营业期限类型") private String businessTermType; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceContact.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceContact.java new file mode 100644 index 0000000..c27d5e5 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceContact.java @@ -0,0 +1,71 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +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 lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** + * 客商发票联系信息实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_customer_invoice_contact") +@Schema(description = "客商发票联系信息") +public class CustomerInvoiceContact extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "发票信息ID") + private Long invoiceId; + + @Schema(description = "联系人") + private String contactName; + + @Schema(description = "联系电话") + private String contactPhone; + + @Schema(description = "邮箱地址") + private String email; + + @Schema(description = "所属部门ID集合") + private String deptIds; + + @Schema(description = "所属部门") + private String deptNames; + + @Schema(description = "备注") + private String remark; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceInfo.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceInfo.java index 896a10c..e24a93a 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceInfo.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerInvoiceInfo.java @@ -50,21 +50,15 @@ public class CustomerInvoiceInfo extends TenantEntity { @Schema(description = "客商ID") private Long customerId; - @Schema(description = "受票方名称") + @Schema(description = "企业全称") private String invoiceTitle; - @Schema(description = "发票类型") - private String invoiceType; - @Schema(description = "纳税人识别号") private String taxNo; @Schema(description = "开户行名称") private String bankName; - @Schema(description = "注册电话") - private String registeredPhone; - @Schema(description = "银行账号") private String bankAccount; @@ -77,24 +71,6 @@ public class CustomerInvoiceInfo extends TenantEntity { @Schema(description = "注册地址详细地址") private String registeredDetailAddress; - @Schema(description = "邮箱") - private String email; - - @Schema(description = "收件人姓名") - private String receiverName; - - @Schema(description = "收件人电话") - private String receiverPhone; - - @Schema(description = "收件人地址") - private String receiverAddress; - - @Schema(description = "收件人地址行政区划") - private String receiverRegionName; - - @Schema(description = "收件人详细地址") - private String receiverDetailAddress; - @Schema(description = "是否默认") private Integer isDefault; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java index 6fe108d..a67969d 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InsuranceOcrTemplate.java @@ -52,6 +52,11 @@ public class InsuranceOcrTemplate extends TenantEntity { @Schema(description = "模板名称") private String name; + /** 车船类型:车辆/船舶。 */ + @TableField("vehicle_type") + @Schema(description = "车船类型:车辆/船舶") + private String vehicleType; + /** 字段映射配置JSON。 */ @TableField("mapping_config") @Schema(description = "字段映射配置JSON") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordExpiryStatVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordExpiryStatVO.java new file mode 100644 index 0000000..645035e --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordExpiryStatVO.java @@ -0,0 +1,55 @@ +/** + * 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.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 年检记录有效期统计 + * + * @author Chill + */ +@Data +@Schema(description = "年检记录有效期统计") +public class AnnualInspectionRecordExpiryStatVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "全部") + private Long total; + + @Schema(description = "30天内") + private Long within30; + + @Schema(description = "已过期") + private Long expired; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java index d1d3c96..b76896a 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java @@ -72,4 +72,18 @@ public class AnnualInspectionRecordVO extends AnnualInspectionRecord { @Schema(description = "更新人姓名") private String updateUserName; + @TableField(exist = false) + @Schema(description = "有效期状态:within30-30天内,expired-已过期") + private String expireStatus; + + @TableField(exist = false) + @Schema(description = "当天日期") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate today; + + @TableField(exist = false) + @Schema(description = "预警截止日期") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate warningDate; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java index a413eeb..1a2f0eb 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java @@ -77,4 +77,12 @@ public class CustomerArchiveVO extends CustomerArchive { @Schema(description = "变更记录") private List changeRecords = new ArrayList<>(); + @TableField(exist = false) + @Schema(description = "是否保存变更记录(仅提交时为 true,保存时不记录)") + private Boolean recordChange; + + @TableField(exist = false) + @Schema(description = "客商类型:external外部客商,internal内部组织") + private String customerKind; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceContactVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceContactVO.java new file mode 100644 index 0000000..73a4c77 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceContactVO.java @@ -0,0 +1,45 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.CustomerInvoiceContact; + +import java.io.Serial; + +/** + * 客商发票联系信息视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "客商发票联系信息") +public class CustomerInvoiceContactVO extends CustomerInvoiceContact { + + @Serial + private static final long serialVersionUID = 1L; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceInfoVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceInfoVO.java index 0d88697..a8d539d 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceInfoVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerInvoiceInfoVO.java @@ -22,12 +22,15 @@ */ package org.springblade.transport.pojo.vo; +import com.baomidou.mybatisplus.annotation.TableField; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.CustomerInvoiceInfo; import java.io.Serial; +import java.util.ArrayList; +import java.util.List; /** * 客商发票信息视图实体类 @@ -42,4 +45,8 @@ public class CustomerInvoiceInfoVO extends CustomerInvoiceInfo { @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) + @Schema(description = "联系信息") + private List contacts = new ArrayList<>(); + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictBizServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictBizServiceImpl.java index dc23249..39398f0 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictBizServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictBizServiceImpl.java @@ -136,7 +136,10 @@ public class DictBizServiceImpl extends ServiceImpl impl @Override public IPage parentList(Map dict, Query query) { - IPage page = this.page(Condition.getPage(query), Condition.getQueryWrapper(dict, DictBiz.class).lambda().eq(DictBiz::getParentId, CommonConstant.TOP_PARENT_ID).orderByAsc(DictBiz::getSort)); + IPage page = this.page(Condition.getPage(query), Condition.getQueryWrapper(dict, DictBiz.class).lambda() + .eq(DictBiz::getParentId, CommonConstant.TOP_PARENT_ID) + .orderByAsc(DictBiz::getSort) + .orderByDesc(DictBiz::getId)); return DictBizWrapper.build().pageVO(page); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictServiceImpl.java index bdd5b15..3b0bf98 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DictServiceImpl.java @@ -123,7 +123,10 @@ public class DictServiceImpl extends ServiceImpl implements ID @Override public IPage parentList(Map dict, Query query) { - IPage page = this.page(Condition.getPage(query), Condition.getQueryWrapper(dict, Dict.class).lambda().eq(Dict::getParentId, CommonConstant.TOP_PARENT_ID).orderByAsc(Dict::getSort)); + IPage page = this.page(Condition.getPage(query), Condition.getQueryWrapper(dict, Dict.class).lambda() + .eq(Dict::getParentId, CommonConstant.TOP_PARENT_ID) + .orderByAsc(Dict::getSort) + .orderByDesc(Dict::getId)); return DictWrapper.build().pageVO(page); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java index 988f72c..e97fa2d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java @@ -47,6 +47,7 @@ import org.springblade.transport.excel.AnnualInspectionRecordExcel; import org.springblade.transport.excel.AnnualInspectionRecordExportExcel; import org.springblade.transport.excel.AnnualInspectionRecordImporter; import org.springblade.transport.pojo.entity.AnnualInspectionRecord; +import org.springblade.transport.pojo.vo.AnnualInspectionRecordExpiryStatVO; import org.springblade.transport.pojo.vo.AnnualInspectionRecordVO; import org.springblade.transport.service.IAnnualInspectionRecordService; import org.springblade.transport.wrapper.AnnualInspectionRecordWrapper; @@ -58,6 +59,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -91,6 +93,7 @@ public class AnnualInspectionRecordController extends BladeController { @ApiOperationSupport(order = 2) @Operation(summary = "分页", description = "传入annualInspectionRecord") public R> list(AnnualInspectionRecordVO annualInspectionRecord, Query query) { + fillExpiryDate(annualInspectionRecord); IPage pages = annualInspectionRecordService.selectAnnualInspectionRecordPage(Condition.getPage(normalizeQuery(query)), annualInspectionRecord); return R.data(pages); } @@ -109,8 +112,17 @@ public class AnnualInspectionRecordController extends BladeController { return R.status(annualInspectionRecordService.deleteLogic(Func.toLongList(ids))); } - @PostMapping("/import-annual-inspection-record") + @GetMapping("/expiry-stat") @ApiOperationSupport(order = 5) + @Operation(summary = "有效期统计", description = "传入annualInspectionRecord") + public R expiryStat(AnnualInspectionRecordVO annualInspectionRecord) { + fillExpiryDate(annualInspectionRecord); + annualInspectionRecord.setExpireStatus(null); + return R.data(annualInspectionRecordService.expiryStat(annualInspectionRecord)); + } + + @PostMapping("/import-annual-inspection-record") + @ApiOperationSupport(order = 6) @Operation(summary = "导入年检记录", description = "传入excel") public R importAnnualInspectionRecord(MultipartFile file, HttpServletResponse response) { List failureList = annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class)); @@ -122,17 +134,18 @@ public class AnnualInspectionRecordController extends BladeController { } @GetMapping("/export-annual-inspection-record") - @ApiOperationSupport(order = 6) + @ApiOperationSupport(order = 7) @Operation(summary = "导出年检记录") public void exportAnnualInspectionRecord(AnnualInspectionRecordVO annualInspectionRecord, @RequestParam(required = false) String ids, HttpServletResponse response) { + fillExpiryDate(annualInspectionRecord); List list = annualInspectionRecordService.exportAnnualInspectionRecord(buildExportQuery(annualInspectionRecord, ids)); ExcelUtil.export(response, "年检记录" + DateUtil.time(), "年检记录表", list, AnnualInspectionRecordExportExcel.class); } @GetMapping("/export-template") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 8) @Operation(summary = "导出模板") public void exportTemplate(HttpServletResponse response) { List list = new ArrayList<>(); @@ -155,6 +168,15 @@ public class AnnualInspectionRecordController extends BladeController { return query; } + private void fillExpiryDate(AnnualInspectionRecordVO annualInspectionRecord) { + if (annualInspectionRecord.getToday() == null) { + annualInspectionRecord.setToday(LocalDate.now()); + } + if (annualInspectionRecord.getWarningDate() == null) { + annualInspectionRecord.setWarningDate(annualInspectionRecord.getToday().plusDays(30)); + } + } + private LambdaQueryWrapper buildExportQuery(AnnualInspectionRecordVO annualInspectionRecord, String ids) { LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() .eq(AnnualInspectionRecord::getIsDeleted, 0) @@ -183,6 +205,15 @@ public class AnnualInspectionRecordController extends BladeController { if (Func.isNotEmpty(annualInspectionRecord.getCreateTimeEnd())) { queryWrapper.le(AnnualInspectionRecord::getCreateTime, annualInspectionRecord.getCreateTimeEnd()); } + if ("within30".equals(annualInspectionRecord.getExpireStatus())) { + queryWrapper.isNotNull(AnnualInspectionRecord::getValidUntilDate) + .ge(AnnualInspectionRecord::getValidUntilDate, annualInspectionRecord.getToday()) + .le(AnnualInspectionRecord::getValidUntilDate, annualInspectionRecord.getWarningDate()); + } + if ("expired".equals(annualInspectionRecord.getExpireStatus())) { + queryWrapper.isNotNull(AnnualInspectionRecord::getValidUntilDate) + .lt(AnnualInspectionRecord::getValidUntilDate, annualInspectionRecord.getToday()); + } return queryWrapper; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.java index 477a993..887d05d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.java @@ -27,7 +27,9 @@ package org.springblade.transport.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Param; import org.springblade.transport.pojo.entity.AnnualInspectionRecord; +import org.springblade.transport.pojo.vo.AnnualInspectionRecordExpiryStatVO; import org.springblade.transport.pojo.vo.AnnualInspectionRecordVO; import java.util.List; @@ -46,6 +48,14 @@ public interface AnnualInspectionRecordMapper extends BaseMapper selectAnnualInspectionRecordPage(IPage page, AnnualInspectionRecordVO annualInspectionRecord); + List selectAnnualInspectionRecordPage(IPage page, @Param("annualInspectionRecord") AnnualInspectionRecordVO annualInspectionRecord); + + /** + * 有效期统计 + * + * @param annualInspectionRecord 查询参数 + * @return 统计结果 + */ + AnnualInspectionRecordExpiryStatVO selectExpiryStat(@Param("annualInspectionRecord") AnnualInspectionRecordVO annualInspectionRecord); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml index 53f87e9..30fa5f3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/AnnualInspectionRecordMapper.xml @@ -26,6 +26,49 @@ + + valid_until_date IS NOT NULL + AND valid_until_date >= #{annualInspectionRecord.today} + AND valid_until_date <= #{annualInspectionRecord.warningDate} + + + + valid_until_date IS NOT NULL + AND valid_until_date < #{annualInspectionRecord.today} + + + + is_deleted = 0 + + AND create_dept = #{annualInspectionRecord.createDept} + + + AND vehicle_type = #{annualInspectionRecord.vehicleType} + + + + AND vehicle_no LIKE #{vehicleNoLike} + + + AND inspection_assessment_date >= #{annualInspectionRecord.inspectionAssessmentDateStart} + + + AND inspection_assessment_date <= #{annualInspectionRecord.inspectionAssessmentDateEnd} + + + AND create_time >= #{annualInspectionRecord.createTimeStart} + + + AND create_time <= #{annualInspectionRecord.createTimeEnd} + + + AND + + + AND + + + + + diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml index a9f80c3..d9df135 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml @@ -16,6 +16,7 @@ + @@ -29,6 +30,7 @@ + @@ -61,6 +63,7 @@ short_name, full_name, customer_nature, + guangxi_top100, unified_credit_code, customer_type, project_name, @@ -74,6 +77,7 @@ dept_name, invoice_tax_rate, business_scope, + network_freight_platform, business_term_type, business_end_date, registered_capital, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerInvoiceContactMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerInvoiceContactMapper.java new file mode 100644 index 0000000..fe40da6 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerInvoiceContactMapper.java @@ -0,0 +1,21 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.transport.pojo.entity.CustomerInvoiceContact; + +/** + * 客商发票联系信息 Mapper 接口 + * + * @author Chill + */ +public interface CustomerInvoiceContactMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IAnnualInspectionRecordService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IAnnualInspectionRecordService.java index b114a0f..61889b3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IAnnualInspectionRecordService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IAnnualInspectionRecordService.java @@ -31,6 +31,7 @@ import org.springblade.core.mp.base.BaseService; import org.springblade.transport.excel.AnnualInspectionRecordExcel; import org.springblade.transport.excel.AnnualInspectionRecordExportExcel; import org.springblade.transport.pojo.entity.AnnualInspectionRecord; +import org.springblade.transport.pojo.vo.AnnualInspectionRecordExpiryStatVO; import org.springblade.transport.pojo.vo.AnnualInspectionRecordVO; import java.util.List; @@ -44,6 +45,8 @@ public interface IAnnualInspectionRecordService extends BaseService selectAnnualInspectionRecordPage(IPage page, AnnualInspectionRecordVO annualInspectionRecord); + AnnualInspectionRecordExpiryStatVO expiryStat(AnnualInspectionRecordVO annualInspectionRecord); + boolean submit(AnnualInspectionRecord annualInspectionRecord); List importAnnualInspectionRecord(List data); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java index 64b88d0..73ddd1a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java @@ -68,6 +68,7 @@ public interface ICustomerArchiveService extends BaseService { /** * 新增或修改客商档案 + *

仅当 {@code customer.recordChange = true}(前端点「提交」)时写入变更记录;「保存」不落变更记录。

* * @param customer 客商档案 * @return 是否成功 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java index d760205..563555b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java @@ -36,13 +36,13 @@ import org.springblade.transport.excel.AnnualInspectionRecordExcel; import org.springblade.transport.excel.AnnualInspectionRecordExportExcel; import org.springblade.transport.mapper.AnnualInspectionRecordMapper; import org.springblade.transport.pojo.entity.AnnualInspectionRecord; +import org.springblade.transport.pojo.vo.AnnualInspectionRecordExpiryStatVO; import org.springblade.transport.pojo.vo.AnnualInspectionRecordVO; import org.springblade.transport.service.IAnnualInspectionRecordService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; -import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -74,6 +74,24 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl> records = parseChangeRecords(contractManage.getChangeRecordJson()); + for (int index = records.size() - 1; index >= 0; index--) { + String changeReason = TransportBusinessSupport.trimToNull( + records.get(index).get("changeReason") == null ? null : String.valueOf(records.get(index).get("changeReason"))); + if (Func.isNotEmpty(changeReason)) { + return changeReason; + } + } + return "重新提交变更"; + } + private boolean canResubmitTemporary(ContractManage contractManage) { String stage = contractManage.getContractStage(); String status = contractManage.getApprovalStatus(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index 4231efa..1a783a6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -46,6 +46,7 @@ import org.springblade.transport.mapper.CustomerChangeRecordMapper; import org.springblade.transport.mapper.CustomerContactMapper; import org.springblade.transport.mapper.CustomerCreditScoreDetailMapper; import org.springblade.transport.mapper.CustomerCreditScoreMapper; +import org.springblade.transport.mapper.CustomerInvoiceContactMapper; import org.springblade.transport.mapper.CustomerInvoiceInfoMapper; import org.springblade.transport.mapper.CustomerReceiptAccountMapper; import org.springblade.transport.mapper.UserCustomerScopeMapper; @@ -59,6 +60,7 @@ import org.springblade.transport.pojo.entity.CustomerChangeRecord; import org.springblade.transport.pojo.entity.CustomerContact; import org.springblade.transport.pojo.entity.CustomerCreditScore; import org.springblade.transport.pojo.entity.CustomerCreditScoreDetail; +import org.springblade.transport.pojo.entity.CustomerInvoiceContact; import org.springblade.transport.pojo.entity.CustomerInvoiceInfo; import org.springblade.transport.pojo.entity.CustomerReceiptAccount; import org.springblade.transport.pojo.vo.CustomerArchiveVO; @@ -67,6 +69,7 @@ import org.springblade.transport.pojo.vo.CustomerContactVO; import org.springblade.transport.pojo.vo.CustomerCreditScoreDetailVO; import org.springblade.transport.pojo.vo.CustomerCreditScoreVO; import org.springblade.transport.pojo.vo.CreditRatingStandardVO; +import org.springblade.transport.pojo.vo.CustomerInvoiceContactVO; import org.springblade.transport.pojo.vo.CustomerInvoiceInfoVO; import org.springblade.transport.pojo.vo.CustomerReceiptAccountVO; import org.springblade.transport.service.ICustomerArchiveService; @@ -108,6 +111,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl invoices) { + private void insertInvoices(Long customerId, CustomerArchiveVO customer) { + List invoices = customer.getInvoices(); + if (Func.isEmpty(invoices)) { + return; + } + boolean internal = isInternalCustomer(customer); for (CustomerInvoiceInfoVO invoiceVO : invoices) { if (Func.isEmpty(invoiceVO.getInvoiceTitle()) && Func.isEmpty(invoiceVO.getTaxNo())) { continue; } - if (Func.isEmpty(invoiceVO.getInvoiceType())) { - throw new ServiceException("发票类型不能为空"); + if (Func.isEmpty(invoiceVO.getInvoiceTitle())) { + throw new ServiceException("企业全称不能为空"); } - if (!List.of("增值税专用发票", "普通发票").contains(invoiceVO.getInvoiceType())) { - throw new ServiceException("发票类型不合法"); + if (Func.isEmpty(invoiceVO.getTaxNo())) { + throw new ServiceException("纳税人识别号不能为空"); + } + if (Func.isEmpty(invoiceVO.getBankName())) { + throw new ServiceException("开户行名称不能为空"); + } + if (Func.isEmpty(invoiceVO.getBankAccount())) { + throw new ServiceException("银行账号不能为空"); + } + if (Func.isEmpty(invoiceVO.getRegisteredRegionName()) && Func.isEmpty(invoiceVO.getRegisteredAddress())) { + throw new ServiceException("注册地址不能为空"); + } + if (Func.isEmpty(invoiceVO.getRegisteredDetailAddress())) { + throw new ServiceException("详细地址不能为空"); } invoiceVO.setRegisteredAddress(buildAddress(invoiceVO.getRegisteredRegionName(), invoiceVO.getRegisteredDetailAddress(), invoiceVO.getRegisteredAddress())); - invoiceVO.setReceiverAddress(buildAddress(invoiceVO.getReceiverRegionName(), invoiceVO.getReceiverDetailAddress(), invoiceVO.getReceiverAddress())); CustomerInvoiceInfo invoice = Objects.requireNonNull(BeanUtil.copyProperties(invoiceVO, CustomerInvoiceInfo.class)); invoice.setId(IdWorker.getId()); invoice.setCustomerId(customerId); invoice.setStatus(STATUS_ENABLED); invoiceInfoMapper.insert(invoice); + insertInvoiceContacts(invoice.getId(), invoiceVO.getContacts(), internal, customer); } } + private void insertInvoiceContacts(Long invoiceId, List contacts, boolean internal, + CustomerArchiveVO customer) { + if (Func.isEmpty(contacts)) { + return; + } + for (CustomerInvoiceContactVO contactVO : contacts) { + if (isBlankInvoiceContact(contactVO)) { + continue; + } + if (Func.isEmpty(contactVO.getContactName())) { + throw new ServiceException("发票联系人不能为空"); + } + if (Func.isEmpty(contactVO.getContactPhone())) { + throw new ServiceException("发票联系电话不能为空"); + } + validateInvoiceContactEmail(contactVO.getEmail()); + if (Func.isNotEmpty(contactVO.getRemark()) && contactVO.getRemark().length() > 200) { + throw new ServiceException("发票联系信息备注最多200个字"); + } + if (internal && Func.isEmpty(contactVO.getDeptIds()) && Func.isEmpty(contactVO.getDeptNames())) { + throw new ServiceException("内部组织客商必须选择发票联系信息所属部门"); + } + if (!internal) { + if (Func.isEmpty(contactVO.getDeptIds())) { + contactVO.setDeptIds(customer.getDeptIds()); + } + if (Func.isEmpty(contactVO.getDeptNames())) { + contactVO.setDeptNames(Func.isNotEmpty(customer.getFullName()) ? customer.getFullName() : customer.getDeptName()); + } + } + CustomerInvoiceContact contact = Objects.requireNonNull(BeanUtil.copyProperties(contactVO, CustomerInvoiceContact.class)); + contact.setId(IdWorker.getId()); + contact.setInvoiceId(invoiceId); + contact.setStatus(STATUS_ENABLED); + invoiceContactMapper.insert(contact); + } + } + + private boolean isBlankInvoiceContact(CustomerInvoiceContactVO contactVO) { + return Func.isEmpty(contactVO.getContactName()) + && Func.isEmpty(contactVO.getContactPhone()) + && Func.isEmpty(contactVO.getEmail()) + && Func.isEmpty(contactVO.getDeptIds()) + && Func.isEmpty(contactVO.getDeptNames()) + && Func.isEmpty(contactVO.getRemark()); + } + + private void validateInvoiceContactEmail(String email) { + if (Func.isEmpty(email)) { + return; + } + if (email.length() > 100) { + throw new ServiceException("发票联系邮箱不能超过100个字"); + } + if (!email.matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) { + throw new ServiceException("发票联系邮箱格式不正确"); + } + } + + private boolean isInternalCustomer(CustomerArchiveVO customer) { + String customerKind = customer.getCustomerKind(); + if (Func.isNotEmpty(customerKind)) { + return "internal".equals(customerKind) || "内部组织".equals(customerKind); + } + return "internal".equals(customer.getCustomerNature()) || "内部组织".equals(customer.getCustomerNature()); + } + private void insertScores(Long customerId, List scores) { for (CustomerCreditScoreVO scoreVO : scores) { if (Func.isEmpty(scoreVO.getQuantificationId()) && Func.isEmpty(scoreVO.getScoreDate())) { @@ -529,6 +619,15 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl customerIds, boolean deleteScores) { contactMapper.update(null, Wrappers.lambdaUpdate().in(CustomerContact::getCustomerId, customerIds).set(CustomerContact::getIsDeleted, 1)); receiptAccountMapper.update(null, Wrappers.lambdaUpdate().in(CustomerReceiptAccount::getCustomerId, customerIds).set(CustomerReceiptAccount::getIsDeleted, 1)); + List invoices = invoiceInfoMapper.selectList(Wrappers.lambdaQuery() + .in(CustomerInvoiceInfo::getCustomerId, customerIds) + .eq(CustomerInvoiceInfo::getIsDeleted, 0)); + if (Func.isNotEmpty(invoices)) { + List invoiceIds = invoices.stream().map(CustomerInvoiceInfo::getId).toList(); + invoiceContactMapper.update(null, Wrappers.lambdaUpdate() + .in(CustomerInvoiceContact::getInvoiceId, invoiceIds) + .set(CustomerInvoiceContact::getIsDeleted, 1)); + } invoiceInfoMapper.update(null, Wrappers.lambdaUpdate().in(CustomerInvoiceInfo::getCustomerId, customerIds).set(CustomerInvoiceInfo::getIsDeleted, 1)); if (deleteScores) { deleteScores(customerIds); @@ -565,12 +664,27 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl loadInvoices(Long customerId) { - return invoiceInfoMapper.selectList(Wrappers.lambdaQuery() - .eq(CustomerInvoiceInfo::getCustomerId, customerId) - .eq(CustomerInvoiceInfo::getIsDeleted, 0) - .orderByDesc(CustomerInvoiceInfo::getIsDefault) - .orderByAsc(CustomerInvoiceInfo::getCreateTime)) - .stream().map(invoice -> Objects.requireNonNull(BeanUtil.copyProperties(invoice, CustomerInvoiceInfoVO.class))).toList(); + List invoices = invoiceInfoMapper.selectList(Wrappers.lambdaQuery() + .eq(CustomerInvoiceInfo::getCustomerId, customerId) + .eq(CustomerInvoiceInfo::getIsDeleted, 0) + .orderByDesc(CustomerInvoiceInfo::getIsDefault) + .orderByAsc(CustomerInvoiceInfo::getCreateTime)); + if (Func.isEmpty(invoices)) { + return new ArrayList<>(); + } + List invoiceIds = invoices.stream().map(CustomerInvoiceInfo::getId).toList(); + Map> contactMap = invoiceContactMapper.selectList(Wrappers.lambdaQuery() + .in(CustomerInvoiceContact::getInvoiceId, invoiceIds) + .eq(CustomerInvoiceContact::getIsDeleted, 0) + .orderByAsc(CustomerInvoiceContact::getCreateTime)) + .stream() + .map(contact -> Objects.requireNonNull(BeanUtil.copyProperties(contact, CustomerInvoiceContactVO.class))) + .collect(Collectors.groupingBy(CustomerInvoiceContactVO::getInvoiceId)); + return invoices.stream().map(invoice -> { + CustomerInvoiceInfoVO invoiceVO = Objects.requireNonNull(BeanUtil.copyProperties(invoice, CustomerInvoiceInfoVO.class)); + invoiceVO.setContacts(contactMap.getOrDefault(invoice.getId(), new ArrayList<>())); + return invoiceVO; + }).toList(); } private List loadScores(Long customerId) { @@ -621,6 +735,16 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl(); @@ -928,6 +1052,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl VEHICLE_TYPES = List.of("车辆", "船舶"); private static final List INSURANCE_FIELD_KEYS = List.of( "保险类型", "保单号", "开始日期", "结束日期", "保额", "保费", "发票号", "开票日期", "备注" ); @@ -64,6 +65,7 @@ public class InsuranceOcrTemplateServiceImpl extends BaseServiceImpl queryWrapper = Wrappers.lambdaQuery() .eq(InsuranceOcrTemplate::getIsDeleted, 0) .like(StringUtil.isNotBlank(insuranceOcrTemplate.getName()), InsuranceOcrTemplate::getName, insuranceOcrTemplate.getName()) + .eq(StringUtil.isNotBlank(insuranceOcrTemplate.getVehicleType()), InsuranceOcrTemplate::getVehicleType, insuranceOcrTemplate.getVehicleType()) .orderByDesc(InsuranceOcrTemplate::getCreateTime); return InsuranceOcrTemplateWrapper.build().pageVO(page(page, queryWrapper)); } @@ -79,6 +81,7 @@ public class InsuranceOcrTemplateServiceImpl extends BaseServiceImpl NAME_MAX_LENGTH) { throw new ServiceException("模板名称不能超过100个字符"); } + if (StringUtil.isBlank(insuranceOcrTemplate.getVehicleType())) { + throw new ServiceException("请选择车船类型"); + } + if (!VEHICLE_TYPES.contains(insuranceOcrTemplate.getVehicleType())) { + throw new ServiceException("车船类型仅支持车辆或船舶"); + } Long nameCount = count(Wrappers.lambdaQuery() .eq(InsuranceOcrTemplate::getIsDeleted, 0) .eq(InsuranceOcrTemplate::getName, insuranceOcrTemplate.getName()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java index 53136b5..3c0ed29 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java @@ -85,6 +85,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl INSURANCE_TYPES = Set.of("交强险", "商业险", "承运人责任险", "货运险", "船舶险"); private static final Set SUPPORT_FILE_TYPES = Set.of("jpg", "jpeg", "png", "bmp"); private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?"); + private static final Pattern POLICY_NO_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$"); private final IBaiduOcrService baiduOcrService; private final IInsuranceOcrTemplateService insuranceOcrTemplateService; @@ -157,6 +158,11 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl invoiceInfos = customer == null ? List.of() : activeInvoiceInfos(customer.getId()); + List invoiceInfos = customer == null ? List.of() : activeInvoiceInfoVOs(customer.getId()); Map result = new LinkedHashMap<>(); result.put("issuerName", first.getPayeeName()); result.put("receiverName", first.getPayerName()); @@ -321,8 +327,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl invoiceInfos = activeInvoiceInfos(customer.getId()); - CustomerInvoiceInfo invoiceInfo = invoiceInfos.stream() + List invoiceInfos = activeInvoiceInfoVOs(customer.getId()); + CustomerInvoiceInfoVO invoiceInfo = invoiceInfos.stream() .filter(item -> Objects.equals(item.getId(), request.getReceiverInvoiceInfoId())) .findFirst().orElseThrow(() -> new ServiceException("请选择受票方有效的开票信息")); String departmentEmails = normalizeDepartmentEmails(request.getDepartmentEmails(), invoiceInfos); @@ -655,8 +661,32 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl invoiceInfoEmails(List invoiceInfos) { - return invoiceInfos.stream().map(CustomerInvoiceInfo::getEmail).filter(Func::isNotEmpty) + private List activeInvoiceInfoVOs(Long customerId) { + List invoiceInfos = activeInvoiceInfos(customerId); + if (Func.isEmpty(invoiceInfos)) { + return List.of(); + } + List invoiceIds = invoiceInfos.stream().map(CustomerInvoiceInfo::getId).toList(); + Map> contactMap = customerInvoiceContactMapper.selectList( + Wrappers.lambdaQuery() + .in(CustomerInvoiceContact::getInvoiceId, invoiceIds) + .eq(CustomerInvoiceContact::getIsDeleted, 0) + .orderByAsc(CustomerInvoiceContact::getCreateTime)) + .stream() + .map(contact -> Objects.requireNonNull(BeanUtil.copyProperties(contact, CustomerInvoiceContactVO.class))) + .collect(Collectors.groupingBy(CustomerInvoiceContactVO::getInvoiceId)); + return invoiceInfos.stream().map(invoice -> { + CustomerInvoiceInfoVO invoiceVO = Objects.requireNonNull(BeanUtil.copyProperties(invoice, CustomerInvoiceInfoVO.class)); + invoiceVO.setContacts(contactMap.getOrDefault(invoice.getId(), new ArrayList<>())); + return invoiceVO; + }).toList(); + } + + private List invoiceInfoEmails(List invoiceInfos) { + return invoiceInfos.stream() + .flatMap(invoice -> (invoice.getContacts() == null ? List.of() : invoice.getContacts()).stream()) + .map(CustomerInvoiceContactVO::getEmail) + .filter(Func::isNotEmpty) .flatMap(value -> splitEmails(value).stream()) .collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(), (first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList(); @@ -708,7 +738,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl invoiceInfos) { + private String normalizeDepartmentEmails(String value, List invoiceInfos) { Map configuredEmails = invoiceInfoEmails(invoiceInfos).stream() .collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(), (first, duplicate) -> first, LinkedHashMap::new)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java index 9d5de88..e854595 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java @@ -12,7 +12,9 @@ import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.UserCache; +import org.springblade.system.pojo.entity.DictBiz; import org.springblade.transport.excel.OtherExpenseRecordExcel; import org.springblade.transport.excel.OtherExpenseRecordExportExcel; import org.springblade.transport.mapper.OtherExpenseRecordMapper; @@ -28,6 +30,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; /** * 其他费用记录 服务实现类 @@ -43,7 +46,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl EXPENSE_TYPES = Set.of("过路费", "停车费", "维修费", "保险费", "年检费", "装卸费", "其他"); + private static final String EXPENSE_TYPE_DICT_CODE = "other_fee_category"; @Override public IPage selectOtherExpenseRecordPage(IPage page, OtherExpenseRecordVO otherExpenseRecord) { @@ -103,7 +106,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl validationErrors = new ArrayList<>(); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getExpenseDate()), "费用日期不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getExpenseType()), "费用类型不能为空"); - org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(otherExpenseRecord.getExpenseType()) && !EXPENSE_TYPES.contains(otherExpenseRecord.getExpenseType()), "费用类型不正确"); + org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(otherExpenseRecord.getExpenseType()) && !loadExpenseTypeValues().contains(otherExpenseRecord.getExpenseType()), "费用类型不正确"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getVehicleType()), "车船类型不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(otherExpenseRecord.getVehicleType()) && !VEHICLE.equals(otherExpenseRecord.getVehicleType()) && !SHIP.equals(otherExpenseRecord.getVehicleType()), "车船类型不正确"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(otherExpenseRecord.getVehicleNo()), "车牌号/船号不能为空"); @@ -152,7 +155,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl loadExpenseTypeValues() { + List expenseTypes = DictBizCache.getList(EXPENSE_TYPE_DICT_CODE); + if (Func.isEmpty(expenseTypes)) { + return Set.of(); + } + return expenseTypes.stream() + .map(DictBiz::getDictValue) + .filter(Objects::nonNull) + .map(String::trim) + .filter(Func::isNotEmpty) + .collect(Collectors.toSet()); + } + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index d5f0187..50749f1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -1565,11 +1565,11 @@ public class ReceivablePayableDetailServiceImpl private BigDecimal calculateRule(Map rule, Waybill waybill) { String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", ""); - BigDecimal base = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice")); + BigDecimal measuredBase = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice")); List> ranges = ranges(rule); boolean intervalUnitPrice = "区间单价".equals(type); boolean intervalFlatPrice = type.contains("区间") && type.contains("一口价"); - if (!intervalUnitPrice && !intervalFlatPrice) base = applyMinimum(base, rule, waybill); + BigDecimal base = (!intervalUnitPrice && !intervalFlatPrice) ? applyMinimum(measuredBase, rule, waybill) : measuredBase; if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP); if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); if (intervalFlatPrice) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); diff --git a/doc/sql/transport/blade_customer_archive.sql b/doc/sql/transport/blade_customer_archive.sql index 680e9fc..640af81 100644 --- a/doc/sql/transport/blade_customer_archive.sql +++ b/doc/sql/transport/blade_customer_archive.sql @@ -9,6 +9,7 @@ CREATE TABLE `blade_customer_archive` ( `short_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客商简称', `full_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客商全称', `customer_nature` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客商性质', + `guangxi_top100` tinyint NULL DEFAULT NULL COMMENT '是否广西百强:0否,1是', `unified_credit_code` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '统一社会信用代码', `customer_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客商类型', `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属项目', @@ -22,6 +23,7 @@ CREATE TABLE `blade_customer_archive` ( `dept_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所属组织', `invoice_tax_rate` decimal(6,2) NULL DEFAULT NULL COMMENT '开票税点', `business_scope` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '经营范围', + `network_freight_platform` tinyint NULL DEFAULT NULL COMMENT '网络货运平台:0否,1是', `business_term_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '营业期限类型', `business_end_date` date NULL DEFAULT NULL COMMENT '营业期限截止日', `registered_capital` decimal(18,2) NULL DEFAULT NULL COMMENT '注册资金(万元)', @@ -116,21 +118,13 @@ CREATE TABLE `blade_customer_invoice_info` ( `id` bigint NOT NULL COMMENT '主键', `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000' COMMENT '租户ID', `customer_id` bigint NOT NULL COMMENT '客商ID', - `invoice_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '受票方名称', - `invoice_type` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发票类型', + `invoice_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '企业全称', `tax_no` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '纳税人识别号', `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开户行名称', - `registered_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册电话', `bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号', `registered_address` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址', `registered_region_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址行政区划', `registered_detail_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址详细地址', - `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱', - `receiver_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收件人姓名', - `receiver_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收件人电话', - `receiver_address` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收件人地址', - `receiver_region_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收件人地址行政区划', - `receiver_detail_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收件人详细地址', `is_default` int NULL DEFAULT 0 COMMENT '是否默认', `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', @@ -143,6 +137,31 @@ CREATE TABLE `blade_customer_invoice_info` ( KEY `idx_customer_invoice_customer` (`tenant_id`, `customer_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客商发票信息'; +-- ---------------------------- +-- Table structure for blade_customer_invoice_contact +-- ---------------------------- +DROP TABLE IF EXISTS `blade_customer_invoice_contact`; +CREATE TABLE `blade_customer_invoice_contact` ( + `id` bigint NOT NULL COMMENT '主键', + `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000' COMMENT '租户ID', + `invoice_id` bigint NOT NULL COMMENT '发票信息ID', + `contact_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系人', + `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系电话', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址', + `dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属部门ID集合', + `dept_names` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属部门', + `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_user` bigint NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', + `status` int NULL DEFAULT 1 COMMENT '状态', + `is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_customer_invoice_contact_invoice` (`tenant_id`, `invoice_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客商发票联系信息'; + -- ---------------------------- -- Table structure for blade_customer_credit_score -- ---------------------------- diff --git a/doc/sql/transport/blade_customer_archive_guangxi_top100_20260914.sql b/doc/sql/transport/blade_customer_archive_guangxi_top100_20260914.sql new file mode 100644 index 0000000..8f56222 --- /dev/null +++ b/doc/sql/transport/blade_customer_archive_guangxi_top100_20260914.sql @@ -0,0 +1,4 @@ +-- 客商档案:新增「是否广西百强」字段(是否,无默认值,未选择时保持 NULL) + +ALTER TABLE `blade_customer_archive` + ADD COLUMN `guangxi_top100` tinyint NULL DEFAULT NULL COMMENT '是否广西百强:0否,1是' AFTER `customer_nature`; diff --git a/doc/sql/transport/blade_customer_archive_network_freight_platform_20260914.sql b/doc/sql/transport/blade_customer_archive_network_freight_platform_20260914.sql new file mode 100644 index 0000000..d28f143 --- /dev/null +++ b/doc/sql/transport/blade_customer_archive_network_freight_platform_20260914.sql @@ -0,0 +1,4 @@ +-- 客商档案:新增「网络货运平台」字段(是否,无默认值) + +ALTER TABLE `blade_customer_archive` + ADD COLUMN `network_freight_platform` tinyint NULL DEFAULT NULL COMMENT '网络货运平台:0否,1是' AFTER `business_scope`; diff --git a/doc/sql/transport/blade_customer_invoice_contact_20260914.sql b/doc/sql/transport/blade_customer_invoice_contact_20260914.sql new file mode 100644 index 0000000..80d4cce --- /dev/null +++ b/doc/sql/transport/blade_customer_invoice_contact_20260914.sql @@ -0,0 +1,63 @@ +-- 客商发票信息:去掉邮寄信息/发票类型/注册电话,改为维护多个联系信息 + +CREATE TABLE IF NOT EXISTS `blade_customer_invoice_contact` ( + `id` bigint NOT NULL COMMENT '主键', + `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000' COMMENT '租户ID', + `invoice_id` bigint NOT NULL COMMENT '发票信息ID', + `contact_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系人', + `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系电话', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址', + `dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属部门ID集合', + `dept_names` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属部门', + `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_user` bigint NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', + `status` int NULL DEFAULT 1 COMMENT '状态', + `is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_customer_invoice_contact_invoice` (`tenant_id`, `invoice_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客商发票联系信息'; + +INSERT INTO `blade_customer_invoice_contact` ( + `id`, `tenant_id`, `invoice_id`, `contact_name`, `contact_phone`, `email`, + `dept_ids`, `dept_names`, `remark`, `create_user`, `create_dept`, `create_time`, + `update_user`, `update_time`, `status`, `is_deleted` +) +SELECT + invoice.`id`, + invoice.`tenant_id`, + invoice.`id`, + invoice.`receiver_name`, + invoice.`receiver_phone`, + invoice.`email`, + NULL, + NULL, + NULL, + invoice.`create_user`, + invoice.`create_dept`, + invoice.`create_time`, + invoice.`update_user`, + invoice.`update_time`, + IFNULL(invoice.`status`, 1), + IFNULL(invoice.`is_deleted`, 0) +FROM `blade_customer_invoice_info` invoice +WHERE (IFNULL(invoice.`receiver_name`, '') <> '' + OR IFNULL(invoice.`receiver_phone`, '') <> '' + OR IFNULL(invoice.`email`, '') <> '') + AND NOT EXISTS ( + SELECT 1 FROM `blade_customer_invoice_contact` contact WHERE contact.`id` = invoice.`id` + ); + +ALTER TABLE `blade_customer_invoice_info` + MODIFY COLUMN `invoice_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '企业全称', + DROP COLUMN `invoice_type`, + DROP COLUMN `registered_phone`, + DROP COLUMN `email`, + DROP COLUMN `receiver_name`, + DROP COLUMN `receiver_phone`, + DROP COLUMN `receiver_address`, + DROP COLUMN `receiver_region_name`, + DROP COLUMN `receiver_detail_address`; diff --git a/doc/sql/transport/blade_insurance_ocr_template.sql b/doc/sql/transport/blade_insurance_ocr_template.sql index 8bd1f7f..7423078 100644 --- a/doc/sql/transport/blade_insurance_ocr_template.sql +++ b/doc/sql/transport/blade_insurance_ocr_template.sql @@ -6,6 +6,7 @@ CREATE TABLE `blade_insurance_ocr_template` ( `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板名称', + `vehicle_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '车辆' COMMENT '车船类型:车辆/船舶', `mapping_config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '字段映射配置JSON', `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', @@ -16,6 +17,7 @@ CREATE TABLE `blade_insurance_ocr_template` ( `is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否已删除', PRIMARY KEY (`id`) USING BTREE, KEY `idx_insurance_ocr_template_name` (`tenant_id`, `name`) USING BTREE, + KEY `idx_insurance_ocr_template_vehicle_type` (`tenant_id`, `vehicle_type`) USING BTREE, KEY `idx_insurance_ocr_template_status` (`status`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='保险OCR识别模板'; diff --git a/doc/sql/transport/blade_insurance_ocr_template_vehicle_type_20260914.sql b/doc/sql/transport/blade_insurance_ocr_template_vehicle_type_20260914.sql new file mode 100644 index 0000000..141d93e --- /dev/null +++ b/doc/sql/transport/blade_insurance_ocr_template_vehicle_type_20260914.sql @@ -0,0 +1,5 @@ +-- 保险OCR识别模板:新增车船类型字段 + +ALTER TABLE `blade_insurance_ocr_template` + ADD COLUMN `vehicle_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '车辆' COMMENT '车船类型:车辆/船舶' AFTER `name`, + ADD KEY `idx_insurance_ocr_template_vehicle_type` (`tenant_id`, `vehicle_type`) USING BTREE; diff --git a/hs_err_pid22822.log b/hs_err_pid22822.log new file mode 100644 index 0000000..9cc29df --- /dev/null +++ b/hs_err_pid22822.log @@ -0,0 +1,157 @@ +# +# A fatal error has been detected by the Java Runtime Environment: +# +# SIGBUS (0xa) at pc=0x0000000104d364c0, pid=22822, tid=5379 +# +# JRE version: OpenJDK Runtime Environment Zulu17.44+15-CA (17.0.8+7) (build 17.0.8+7-LTS) +# Java VM: OpenJDK 64-Bit Server VM Zulu17.44+15-CA (17.0.8+7-LTS, mixed mode, emulated-client, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64) +# Problematic frame: +# C [libzip.dylib+0x64c0] newEntry+0x68 +# +# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again +# +# If you would like to submit a bug report, please visit: +# http://www.azul.com/support/ +# The crash happened outside the Java Virtual Machine in native code. +# See problematic frame for where to report the bug. +# + +--------------- S U M M A R Y ------------ + +Command Line: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:54448,suspend=y,server=n -javaagent:/Users/liangxin/Library/Caches/JetBrains/IntelliJIdea2026.1/captureAgent/debugger-agent.jar=file:///var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/capture14073385024433055703.props -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Ddebugger.async.stack.trace.for.all.threads=true -Dfile.encoding=UTF-8 org.springblade.admin.AdminApplication + +Host: "Mac15,6" arm64, 12 cores, 36G, Darwin 25.6.0, macOS 26.6.2 (25G83) +Time: Mon Sep 14 22:15:03 2026 CST elapsed time: 5.909458 seconds (0d 0h 0m 5s) + +--------------- T H R E A D --------------- + +Current thread (0x000000013601ac00): JavaThread "main" [_thread_in_native, id=5379, stack(0x000000016b250000,0x000000016b453000)] + +Stack: [0x000000016b250000,0x000000016b453000], sp=0x000000016b450260, free space=2048k +Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) +C [libzip.dylib+0x64c0] newEntry+0x68 +C [libzip.dylib+0x6390] ZIP_GetEntry2+0x14c +C [libzip.dylib+0x6d78] ZIP_FindEntry+0x3c +V [libjvm.dylib+0x25a408] ClassPathZipEntry::open_entry(JavaThread*, char const*, int*, bool)+0xb4 +V [libjvm.dylib+0x25a53c] ClassPathZipEntry::open_stream(JavaThread*, char const*)+0x20 +V [libjvm.dylib+0x25d918] ClassLoader::load_class(Symbol*, bool, JavaThread*)+0x150 +V [libjvm.dylib+0x981d60] SystemDictionary::load_instance_class_impl(Symbol*, Handle, JavaThread*)+0x2d0 +V [libjvm.dylib+0x98063c] SystemDictionary::load_instance_class(unsigned int, Symbol*, Handle, JavaThread*)+0x30 +V [libjvm.dylib+0x97fd48] SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, JavaThread*)+0x4dc +V [libjvm.dylib+0x97f334] SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, JavaThread*)+0x80 +V [libjvm.dylib+0x2beb54] ConstantPool::klass_at_impl(constantPoolHandle const&, int, JavaThread*)+0x1e0 +V [libjvm.dylib+0x46d6f0] InterpreterRuntime::_new(JavaThread*, ConstantPool*, int)+0x94 +j com.intellij.rt.debugger.agent.CaptureStorage.createCapturedStack(Ljava/lang/Throwable;Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;)Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;+14 +j com.intellij.rt.debugger.agent.CaptureStorage.access$500(Ljava/lang/Throwable;Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;)Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$3.run()V+75 +j com.intellij.rt.debugger.agent.OverheadDetector$PerThread.runIfNoOverhead(Ljava/lang/Runnable;)Z+62 +j com.intellij.rt.debugger.agent.CaptureStorage.runWithOverheadTrackingAndWithoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$ThreadLocalContext;Ljava/lang/Runnable;)Z+15 +j com.intellij.rt.debugger.agent.CaptureStorage.capture(Ljava/lang/Object;)V+26 +j java.util.concurrent.FutureTask.(Ljava/util/concurrent/Callable;)V+5 java.base@17.0.8 +j org.springframework.cglib.core.internal.LoadingCache.createEntry(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;+27 +j org.springframework.cglib.core.internal.LoadingCache.get(Ljava/lang/Object;)Ljava/lang/Object;+39 +j org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(Lorg/springframework/cglib/core/AbstractClassGenerator;Z)Ljava/lang/Object;+11 +j org.springframework.cglib.core.AbstractClassGenerator.create(Ljava/lang/Object;)Ljava/lang/Object;+115 +j org.springframework.cglib.reflect.FastClass$Generator.create()Lorg/springframework/cglib/reflect/FastClass;+19 +j org.springframework.cglib.proxy.MethodProxy.helper(Lorg/springframework/cglib/proxy/MethodProxy$CreateInfo;Ljava/lang/Class;)Lorg/springframework/cglib/reflect/FastClass;+54 +j org.springframework.cglib.proxy.MethodProxy.init()V+40 +j org.springframework.cglib.proxy.MethodProxy.create(Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/springframework/cglib/proxy/MethodProxy;+80 +j org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration$BlockingLoadBalancerRetryConfig$$SpringCGLIB$$0.CGLIB$STATICHOOK1()V+112 +j org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration$BlockingLoadBalancerRetryConfig$$SpringCGLIB$$0.()V+3 +v ~StubRoutines::call_stub +V [libjvm.dylib+0x4781f8] JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)+0x394 +V [libjvm.dylib+0x457c84] InstanceKlass::call_class_initializer(JavaThread*)+0x1e8 +V [libjvm.dylib+0x456f48] InstanceKlass::initialize_impl(JavaThread*)+0x65c +V [libjvm.dylib+0x51e020] JVM_FindClassFromCaller+0x340 +C [libjava.dylib+0x3a6c] Java_java_lang_Class_forName0+0x138 +J 1317 java.lang.Class.forName0(Ljava/lang/String;ZLjava/lang/ClassLoader;Ljava/lang/Class;)Ljava/lang/Class; java.base@17.0.8 (0 bytes) @ 0x000000010ed5306c [0x000000010ed52fc0+0x00000000000000ac] +V [libjvm.dylib+0x4715a0] InterpreterRuntime::resolve_from_cache(JavaThread*, Bytecodes::Code)+0x98 + +Java frames: (J=compiled Java code, j=interpreted, Vv=VM code) +j com.intellij.rt.debugger.agent.CaptureStorage.createCapturedStack(Ljava/lang/Throwable;Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;)Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;+14 +j com.intellij.rt.debugger.agent.CaptureStorage.access$500(Ljava/lang/Throwable;Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;)Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$3.run()V+75 +j com.intellij.rt.debugger.agent.OverheadDetector$PerThread.runIfNoOverhead(Ljava/lang/Runnable;)Z+62 +j com.intellij.rt.debugger.agent.CaptureStorage.runWithOverheadTrackingAndWithoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$ThreadLocalContext;Ljava/lang/Runnable;)Z+15 +j com.intellij.rt.debugger.agent.CaptureStorage.capture(Ljava/lang/Object;)V+26 +j java.util.concurrent.FutureTask.(Ljava/util/concurrent/Callable;)V+5 java.base@17.0.8 +j org.springframework.cglib.core.internal.LoadingCache.createEntry(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;+27 +j org.springframework.cglib.core.internal.LoadingCache.get(Ljava/lang/Object;)Ljava/lang/Object;+39 +j org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(Lorg/springframework/cglib/core/AbstractClassGenerator;Z)Ljava/lang/Object;+11 +j org.springframework.cglib.core.AbstractClassGenerator.create(Ljava/lang/Object;)Ljava/lang/Object;+115 +j org.springframework.cglib.reflect.FastClass$Generator.create()Lorg/springframework/cglib/reflect/FastClass;+19 +j org.springframework.cglib.proxy.MethodProxy.helper(Lorg/springframework/cglib/proxy/MethodProxy$CreateInfo;Ljava/lang/Class;)Lorg/springframework/cglib/reflect/FastClass;+54 +j org.springframework.cglib.proxy.MethodProxy.init()V+40 +j org.springframework.cglib.proxy.MethodProxy.create(Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/springframework/cglib/proxy/MethodProxy;+80 +j org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration$BlockingLoadBalancerRetryConfig$$SpringCGLIB$$0.CGLIB$STATICHOOK1()V+112 +j org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration$BlockingLoadBalancerRetryConfig$$SpringCGLIB$$0.()V+3 +v ~StubRoutines::call_stub +J 1317 java.lang.Class.forName0(Ljava/lang/String;ZLjava/lang/ClassLoader;Ljava/lang/Class;)Ljava/lang/Class; java.base@17.0.8 (0 bytes) @ 0x000000010ed5306c [0x000000010ed52fc0+0x00000000000000ac] +J 1432 c1 java.lang.Class.forName(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class; java.base@17.0.8 (47 bytes) @ 0x000000010ed807c8 [0x000000010ed806c0+0x0000000000000108] +j org.springframework.cglib.core.ReflectUtils.defineClass(Ljava/lang/String;[BLjava/lang/ClassLoader;Ljava/security/ProtectionDomain;Ljava/lang/Class;)Ljava/lang/Class;+460 +j org.springframework.cglib.core.AbstractClassGenerator.generate(Lorg/springframework/cglib/core/AbstractClassGenerator$ClassLoaderData;)Ljava/lang/Class;+209 +j org.springframework.cglib.proxy.Enhancer.generate(Lorg/springframework/cglib/core/AbstractClassGenerator$ClassLoaderData;)Ljava/lang/Class;+53 +j org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(Lorg/springframework/cglib/core/AbstractClassGenerator;)Ljava/lang/Object;+2 +j org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$$Lambda$857+0x00000070016c9418.apply(Ljava/lang/Object;)Ljava/lang/Object;+8 +j org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(Ljava/lang/Object;)Ljava/lang/Object;+5 +j org.springframework.cglib.core.internal.LoadingCache$$Lambda$859+0x00000070016c9a98.call()Ljava/lang/Object;+8 +j java.util.concurrent.FutureTask.run$$$capture()V+39 java.base@17.0.8 +j java.util.concurrent.FutureTask.run()V+5 java.base@17.0.8 +j org.springframework.cglib.core.internal.LoadingCache.createEntry(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;+56 +j org.springframework.cglib.core.internal.LoadingCache.get(Ljava/lang/Object;)Ljava/lang/Object;+39 +j org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(Lorg/springframework/cglib/core/AbstractClassGenerator;Z)Ljava/lang/Object;+11 +j org.springframework.cglib.core.AbstractClassGenerator.create(Ljava/lang/Object;)Ljava/lang/Object;+115 +j org.springframework.cglib.proxy.Enhancer.createHelper()Ljava/lang/Object;+102 +j org.springframework.cglib.proxy.Enhancer.createClass()Ljava/lang/Class;+6 +j org.springframework.context.annotation.ConfigurationClassEnhancer.createClass(Lorg/springframework/cglib/proxy/Enhancer;Z)Ljava/lang/Class;+1 +j org.springframework.context.annotation.ConfigurationClassEnhancer.enhance(Ljava/lang/Class;Ljava/lang/ClassLoader;)Ljava/lang/Class;+134 +j org.springframework.context.annotation.ConfigurationClassPostProcessor.enhanceConfigurationClasses(Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)V+418 +j org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanFactory(Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)V+78 +j org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(Ljava/util/Collection;Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)V+61 +j org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;Ljava/util/List;)V+521 +j org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)V+5 +j org.springframework.context.support.AbstractApplicationContext.refresh()V+62 +j org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh()V+1 +j org.springframework.boot.SpringApplication.refresh(Lorg/springframework/context/ConfigurableApplicationContext;)V+1 +j org.springframework.boot.SpringApplication.refreshContext(Lorg/springframework/context/ConfigurableApplicationContext;)V+19 +j org.springframework.boot.SpringApplication.run([Ljava/lang/String;)Lorg/springframework/context/ConfigurableApplicationContext;+113 +j org.springframework.boot.builder.SpringApplicationBuilder.run([Ljava/lang/String;)Lorg/springframework/context/ConfigurableApplicationContext;+38 +j org.springblade.core.launch.BladeApplication.run(Ljava/lang/String;Ljava/lang/Class;[Ljava/lang/String;)Lorg/springframework/context/ConfigurableApplicationContext;+9 +j org.springblade.admin.AdminApplication.main([Ljava/lang/String;)V+5 +v ~StubRoutines::call_stub + +siginfo: si_signo: 10 (SIGBUS), si_code: 1 (BUS_ADRALN), si_addr: 0x0000000104cfe958 + +Register to memory mapping: + + x0=0x0000600001ebc820 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x1=0x0 is NULL + x2=0xfffffffffffffff0 is an unknown value + x3=0x0000600001ebc830 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x4=0x0000600001ebc880 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x5=0x000000009b858ffb is an unknown value + x6=0x000000001ba00000 is an unknown value + x7=0x00000005c0183f80 is an oop: com.intellij.rt.debugger.agent.CaptureStorage$ConcurrentIdentityWeakHashMap$WeakKey +{0x00000005c0183f80} - klass: 'com/intellij/rt/debugger/agent/CaptureStorage$ConcurrentIdentityWeakHashMap$WeakKey' + - ---- fields (total size 4 words): + - private 'referent' 'Ljava/lang/Object;' @12 a 'java/lang/Thread'{0x00000005c0203dc0} (b80407b8) + - volatile 'queue' 'Ljava/lang/ref/ReferenceQueue;' @16 a 'java/lang/ref/ReferenceQueue'{0x00000005c0183ea0} (b80307d4) + - volatile 'next' 'Ljava/lang/ref/Reference;' @20 NULL (0) + - private transient 'discovered' 'Ljava/lang/ref/Reference;' @24 NULL (0) + - private final 'myHash' 'I' @28 897913732 (35851384) + x8=0x0000000104e2693c: getProcessHandle.procHandle+0x971c in /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjava.dylib at 0x0000000104e00000 + x9=0x0000000000128000 is an unknown value +x10=0x0000600001ebc000 points into unknown readable memory: 0x4c2800498af20001 | 01 00 f2 8a 49 00 28 4c +x11=0x0000000000000820 is an unknown value +x12=0x0000000000000050 is an unknown value +x13=0x0000000000000001 is an unknown value +x14=0x00000000ffffff4e is an unknown value +x15=0x00000000000007fb is an unknown value +x16=0x00000001829fd030: __bzero+0 in /usr/lib/system/libsystem_platform.dylib at 0x00000001829fa000 +x17=0x00000001f0a754a8 points into unknown readable memory: 0x00000001829fd030 | 30 d0 9f 82 01 00 00 00 +x18=0x0 is NULL +x19=0x0000600001ebc820 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x20=0x0 is NULL +x21=0x0000600000bf4240 points into unknown readable memory: 0x0000600001af00c0 | c0 00 af 01 00 60 00 00 +x22=0x0000000104cfe93c points into unknown readable memory: 50 4b 01 02 +x23= \ No newline at end of file From 0d1f14f703900c199269432824ed5c3a9a4d6cc9 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 15 Sep 2026 11:59:42 +0800 Subject: [PATCH 094/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E5=90=88=E5=90=8C?= =?UTF-8?q?=E3=80=81=E8=BF=90=E5=8A=9B=E3=80=81=E5=AE=A2=E5=95=86=E3=80=81?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springblade/system/pojo/entity/Dept.java | 6 + .../transport/pojo/vo/CustomerArchiveVO.java | 17 ++ .../transport/pojo/vo/VehicleDispatchVO.java | 3 + .../system/controller/DeptController.java | 13 +- .../springblade/system/mapper/DeptMapper.xml | 2 + .../system/service/IDeptService.java | 7 + .../system/service/impl/DeptServiceImpl.java | 19 +- .../controller/ContractManageController.java | 2 +- .../transport/excel/CustomerArchiveExcel.java | 38 ++-- .../transport/excel/ProjectApplyExcel.java | 2 +- .../transport/excel/VehicleDispatchExcel.java | 2 + .../mapper/VehicleDispatchMapper.xml | 28 +-- .../impl/ContractManageServiceImpl.java | 2 - .../impl/CustomerArchiveServiceImpl.java | 176 +++++++++++++++++- .../service/impl/ProjectApplyServiceImpl.java | 4 +- .../impl/TemporaryCreditLimitServiceImpl.java | 3 - .../wrapper/VehicleDispatchWrapper.java | 5 + doc/sql/bladex/bladex.mysql.all.create.sql | 1 + ...lade_dept_is_platform_company_20260915.sql | 4 + 19 files changed, 285 insertions(+), 49 deletions(-) create mode 100644 doc/sql/transport/blade_dept_is_platform_company_20260915.sql diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java index d0fb928..2e075ec 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java @@ -179,4 +179,10 @@ public class Dept extends TenantEntity { @Schema(description = "是否是oa的部门") private Integer isOa; + /** + * 是否平台公司:0否,1是 + */ + @Schema(description = "是否平台公司:0否,1是") + private Integer isPlatformCompany; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java index 1a2f0eb..bd1befa 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java @@ -30,6 +30,7 @@ import org.springblade.transport.pojo.entity.CustomerArchive; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serial; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -85,4 +86,20 @@ public class CustomerArchiveVO extends CustomerArchive { @Schema(description = "客商类型:external外部客商,internal内部组织") private String customerKind; + @TableField(exist = false) + @Schema(description = "资金使用风险等级:high、medium、none") + private String fundUseRisk; + + @TableField(exist = false) + @Schema(description = "资金使用风险名称") + private String fundUseRiskName; + + @TableField(exist = false) + @Schema(description = "资金使用率(百分比)") + private BigDecimal fundUseRate; + + @TableField(exist = false) + @Schema(description = "已付金额合计(元)") + private BigDecimal usedFundLimit; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java index 05ed2f9..91544a8 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VehicleDispatchVO.java @@ -50,6 +50,9 @@ public class VehicleDispatchVO extends VehicleDispatch { @Schema(description = "审批状态名称") private String approvalStatusName; @TableField(exist = false) + @Schema(description = "车辆类型") + private String vehicleType; + @TableField(exist = false) @Schema(description = "创建人姓名") private String createUserName; @TableField(exist = false) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 06d1eb9..11c4081 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -189,12 +189,23 @@ public class DeptController extends BladeController { return R.data(deptService.selectDept(deptId)); } + /** + * 平台公司下拉(是否平台公司=是) + */ + @PreAuth(AuthConstant.PERMIT_ALL) + @GetMapping("/platform-company-select") + @ApiOperationSupport(order = 9) + @Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表") + public R> platformCompanySelect() { + return R.data(deptService.listPlatformCompany()); + } + /** * 获取部门的主管信息 */ @IsAdmin @GetMapping("/dept-leader-info") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 10) @Operation(summary = "获取部门的主管信息", description = "传入deptId") public R> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) { List list = deptService.deptLeaderInfo(deptId); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml index 4b40dea..baef84d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml @@ -16,6 +16,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java index 22b05cd..48e5102 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java @@ -97,6 +97,13 @@ public interface IDeptService extends IService { */ List selectDept(String deptId); + /** + * 平台公司下拉(是否平台公司=是) + * + * @return 平台公司部门列表 + */ + List listPlatformCompany(); + /** * 根据部门名称精确匹配获取部门ID集合 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java index 53b4b7b..ff25a47 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java @@ -151,6 +151,18 @@ public class DeptServiceImpl extends ServiceImpl implements ID return baseMapper.selectList(queryWrapper); } + @Override + public List listPlatformCompany() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(Dept::getIsPlatformCompany, 1) + .orderByAsc(Dept::getSort) + .orderByAsc(Dept::getId); + if (!AuthUtil.isAdministrator()) { + queryWrapper.eq(Dept::getTenantId, AuthUtil.getTenantId()); + } + return list(queryWrapper); + } + @Override public String getDeptIds(String tenantId, String deptNames) { List deptList = baseMapper.selectList(Wrappers.query().lambda().eq(Dept::getTenantId, tenantId).in(Dept::getDeptName, Func.toStrList(deptNames))); @@ -232,6 +244,9 @@ public class DeptServiceImpl extends ServiceImpl implements ID dept.setAncestors(parent.getAncestors() + StringPool.COMMA + dept.getParentId()); } dept.setIsDeleted(BladeConstant.DB_NOT_DELETED); + if (dept.getIsPlatformCompany() == null) { + dept.setIsPlatformCompany(0); + } if (Func.isEmpty(dept.getTenantId())) { throw new ServiceException("租户ID不能为空"); } @@ -264,8 +279,10 @@ public class DeptServiceImpl extends ServiceImpl implements ID private void validateDeptCode(Dept dept, Dept parent) { String deptCode = dept.getDeptCode(); + // 组织编码非必填,为空时存 null,避免唯一索引冲突 if (StringUtil.isBlank(deptCode)) { - throw new ServiceException("组织编码不能为空"); + dept.setDeptCode(null); + return; } deptCode = deptCode.trim(); if (deptCode.length() > 30) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java index 92a11df..b6d2710 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractManageController.java @@ -132,7 +132,7 @@ public class ContractManageController extends BladeController { @Operation(summary = "发起变更", description = "传入id、changeContent和changeReason") public R startChange(@Parameter(description = "主键", required = true) @RequestParam Long id, @RequestParam(required = false) String changeContent, - @RequestParam String changeReason) { + @RequestParam(required = false) String changeReason) { return R.status(contractManageService.startChange(id, changeContent, changeReason)); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java index a18a875..cc15e8b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java @@ -54,36 +54,24 @@ public class CustomerArchiveExcel implements Serializable { @ExcelProperty("客商简称") private String shortName; - @ExcelProperty("*客商名称") + @ExcelProperty("客商名称") private String fullName; - @ExcelProperty("*客商类型") + @ExcelProperty("客商类型") private String customerType; - @ExcelProperty("*客商性质") + @ExcelProperty("客商性质") private String customerNature; - @ExcelProperty("*统一信用代码") + @ExcelProperty("统一信用代码") private String unifiedCreditCode; - @ExcelProperty("*所属组织") + @ExcelProperty("所属组织") private String deptName; @ExcelProperty("准入类型") private String accessTypeName; - @ExcelProperty("审批状态") - private String approvalStatusName; - - @ExcelProperty("当前节点") - private String currentNode; - - @ExcelProperty("当前处理人") - private String currentProcessor; - - @ExcelProperty("审核通过时间") - private LocalDateTime approvedTime; - @ExcelProperty("状态") private String statusName; @@ -96,13 +84,25 @@ public class CustomerArchiveExcel implements Serializable { @ExcelProperty("申请总资金使用额度(万元)") private BigDecimal applyCreditLimit; - @ExcelProperty("*联系电话") + @ExcelProperty("联系电话") private String contactPhone; - @ExcelProperty("*法人/负责人") + @ExcelProperty("法人/负责人") private String legalPerson; @ExcelProperty("创建时间") private Date createTime; + @ExcelProperty("审批状态") + private String approvalStatusName; + + @ExcelProperty("当前节点") + private String currentNode; + + @ExcelProperty("当前处理人") + private String currentProcessor; + + @ExcelProperty("审核通过时间") + private LocalDateTime approvedTime; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java index cf8bdaa..8bd25d4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ProjectApplyExcel.java @@ -60,7 +60,7 @@ public class ProjectApplyExcel implements Serializable { private String projectType; @ExcelProperty("业务部门") private String businessDeptName; - @ExcelProperty("承办部门") + @ExcelProperty("平台公司") private String undertakeDeptName; @ExcelProperty("项目由来") private String projectSource; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java index 32f4cf6..c24e9ee 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleDispatchExcel.java @@ -30,6 +30,7 @@ public class VehicleDispatchExcel implements Serializable { @ExcelProperty("车牌号") private String plateNo; @ExcelProperty("所属组织") private String organizationName; @ExcelProperty("使用部门") private String useDepartment; + @ExcelProperty("车辆类型") private String vehicleType; @ExcelProperty("审批状态") private String approvalStatusName; @ExcelProperty("当前节点") private String currentNode; @ExcelProperty("当前处理人") private String currentProcessor; @@ -42,6 +43,7 @@ public class VehicleDispatchExcel implements Serializable { target.plateNo = source.getPlateNo(); target.organizationName = source.getOrganizationName(); target.useDepartment = source.getUseDepartment(); + target.vehicleType = source.getVehicleType(); target.approvalStatusName = source.getApprovalStatusName(); target.currentNode = source.getCurrentNode(); target.currentProcessor = source.getCurrentProcessor(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml index e372641..72a9bfb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VehicleDispatchMapper.xml @@ -8,6 +8,7 @@ + @@ -21,26 +22,31 @@ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index 663521e..aea3954 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -251,7 +251,6 @@ public class ContractManageServiceImpl extends BaseServiceImpl CHANGE_SNAPSHOT_IGNORE_FIELDS = Set.of( + "id", "customerId", "invoiceId", "scoreId", "tenantId", + "createUser", "createDept", "createTime", "updateUser", "updateTime", + "isDeleted", "status", "regionPath", "deptIdList", "registeredRegionPath", + "fundUseAmount", "fundUseRate", "fundUseRisk", "standards", "attachments" + ); private final CustomerContactMapper contactMapper; private final CustomerReceiptAccountMapper receiptAccountMapper; @@ -121,10 +136,13 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl selectCustomerArchivePage(IPage page, CustomerArchiveVO customer) { - return page.setRecords(baseMapper.selectCustomerArchivePage(page, customer, AuthUtil.getUserId())); + List records = baseMapper.selectCustomerArchivePage(page, customer, AuthUtil.getUserId()); + fillFundUseRisk(records); + return page.setRecords(records); } @Override @@ -156,6 +174,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl snapshot, CustomerArchiveVO detail) { if (detail == null) { return; } - snapshot.put("联系人信息", JsonUtil.toJson(detail.getContacts())); - snapshot.put("收款信息", JsonUtil.toJson(detail.getReceiptAccounts())); - snapshot.put("发票信息", JsonUtil.toJson(detail.getInvoices())); - snapshot.put("评分信息", JsonUtil.toJson(detail.getScores())); + snapshot.put("联系人信息", toBusinessSnapshotJson(detail.getContacts())); + snapshot.put("收款信息", toBusinessSnapshotJson(detail.getReceiptAccounts())); + snapshot.put("发票信息", toBusinessSnapshotJson(detail.getInvoices())); + snapshot.put("评分信息", toBusinessSnapshotJson(detail.getScores())); + } + + /** + * 明细快照只保留业务字段,并忽略主键/审计字段,避免“无修改提交”因重建 ID 误记变更。 + */ + private String toBusinessSnapshotJson(Object value) { + Object parsed = JsonUtil.parse(JsonUtil.toJson(value == null ? List.of() : value), Object.class); + return JsonUtil.toJson(normalizeSnapshotNode(parsed)); + } + + @SuppressWarnings("unchecked") + private Object normalizeSnapshotNode(Object node) { + if (node == null) { + return null; + } + if (node instanceof Map map) { + Map result = new TreeMap<>(); + map.forEach((key, value) -> { + String field = String.valueOf(key); + if (CHANGE_SNAPSHOT_IGNORE_FIELDS.contains(field)) { + return; + } + Object normalized = normalizeSnapshotNode(value); + if (normalized == null || "".equals(normalized)) { + return; + } + if (normalized instanceof Map nestedMap && nestedMap.isEmpty()) { + return; + } + if (normalized instanceof List nestedList && nestedList.isEmpty()) { + return; + } + result.put(field, normalized); + }); + return result; + } + if (node instanceof List list) { + List result = list.stream() + .map(this::normalizeSnapshotNode) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(ArrayList::new)); + result.sort(Comparator.comparing(JsonUtil::toJson)); + return result; + } + if (node instanceof BigDecimal decimal) { + return decimal.stripTrailingZeros().toPlainString(); + } + if (node instanceof Number number) { + return new BigDecimal(number.toString()).stripTrailingZeros().toPlainString(); + } + if (node instanceof Boolean || node instanceof LocalDate || node instanceof LocalDateTime) { + return node; + } + String text = String.valueOf(node).trim(); + return Func.isEmpty(text) || "null".equalsIgnoreCase(text) ? null : text; } private Map customerSnapshot(CustomerArchive customer) { @@ -1085,6 +1186,65 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl records) { + if (records == null || records.isEmpty()) { + return; + } + Set payerNames = new HashSet<>(); + records.forEach(customer -> { + if (Func.isNotEmpty(customer.getFullName())) { + payerNames.add(customer.getFullName().trim()); + } + if (Func.isNotEmpty(customer.getShortName())) { + payerNames.add(customer.getShortName().trim()); + } + }); + Map paidAmountByPayer = new HashMap<>(); + if (!payerNames.isEmpty()) { + paymentApplicationMapper.selectList(Wrappers.lambdaQuery() + .in(PaymentApplication::getPayerName, payerNames) + .eq(PaymentApplication::getApprovalStatus, APPROVAL_APPROVED) + .eq(PaymentApplication::getIsDeleted, 0)) + .forEach(item -> { + String payerName = Func.toStr(item.getPayerName()).trim(); + if (Func.isEmpty(payerName)) { + return; + } + paidAmountByPayer.merge(payerName, nonNegative(item.getPaidAmount()), BigDecimal::add); + }); + } + records.forEach(customer -> { + BigDecimal usedFundLimit = BigDecimal.ZERO; + if (Func.isNotEmpty(customer.getFullName())) { + usedFundLimit = usedFundLimit.add(paidAmountByPayer.getOrDefault(customer.getFullName().trim(), BigDecimal.ZERO)); + } + if (Func.isNotEmpty(customer.getShortName())) { + String shortName = customer.getShortName().trim(); + if (!shortName.equals(Func.toStr(customer.getFullName()).trim())) { + usedFundLimit = usedFundLimit.add(paidAmountByPayer.getOrDefault(shortName, BigDecimal.ZERO)); + } + } + BigDecimal maxCreditLimitYuan = nonNegative(customer.getMaxCreditLimit()).multiply(BigDecimal.valueOf(10000)); + BigDecimal fundUseRate = maxCreditLimitYuan.signum() == 0 + ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) + : usedFundLimit.multiply(BigDecimal.valueOf(100)).divide(maxCreditLimitYuan, 2, RoundingMode.HALF_UP); + String risk = fundUseRate.compareTo(new BigDecimal("90")) >= 0 ? "high" + : fundUseRate.compareTo(new BigDecimal("80")) >= 0 ? "medium" : "none"; + customer.setUsedFundLimit(usedFundLimit); + customer.setFundUseRate(fundUseRate); + customer.setFundUseRisk(risk); + customer.setFundUseRiskName("high".equals(risk) ? "高风险" : "medium".equals(risk) ? "中风险" : "无风险"); + }); + } + + private BigDecimal nonNegative(BigDecimal value) { + return value == null || value.signum() < 0 ? BigDecimal.ZERO : value; + } + private CustomerArchiveExcel buildExcel(CustomerArchive customer) { CustomerArchiveExcel excel = new CustomerArchiveExcel(); excel.setCustomerCode(customer.getCustomerCode()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index d30273d..0a58318 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -683,7 +683,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl labels = Map.ofEntries( Map.entry("projectType", "项目类型"), Map.entry("projectName", "项目名称"), Map.entry("projectShortName", "项目简称"), Map.entry("businessDeptName", "业务部门"), - Map.entry("undertakeDeptName", "承办部门"), Map.entry("projectSource", "项目由来"), + Map.entry("undertakeDeptName", "平台公司"), Map.entry("projectSource", "项目由来"), Map.entry("sourceRemark", "项目由来说明"), Map.entry("fundLimit", "项目资金使用额度"), Map.entry("receivableLimit", "项目应收账款额度"), Map.entry("receivableDays", "应收账款回款期限"), Map.entry("paymentDays", "回款账期"), Map.entry("cargoType", "货物类型"), @@ -717,7 +717,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl Date: Tue, 15 Sep 2026 17:52:34 +0800 Subject: [PATCH 095/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E4=B8=9A=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/vo/LoadingManageVO.java | 4 + .../transport/pojo/vo/TransportPlanVO.java | 22 +++++ .../controller/LoadingManageController.java | 13 ++- .../service/ILoadingManageService.java | 2 + .../impl/LoadingManageServiceImpl.java | 95 ++++++++++++++++++- .../service/impl/MasterOrderServiceImpl.java | 2 +- .../impl/TransportPlanServiceImpl.java | 14 ++- .../service/impl/WaybillServiceImpl.java | 9 +- 8 files changed, 151 insertions(+), 10 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java index c2ce78f..f576efd 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java @@ -46,6 +46,10 @@ public class LoadingManageVO extends LoadingManage { @Schema(description = "业务状态名称") private String businessStatusName; + @TableField(exist = false) + @Schema(description = "是否存在司机拒绝接单(可重新派单)") + private Boolean driverRejected; + @TableField(exist = false) @Schema(description = "运单号") private String waybillNo; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java index a8d394d..49e6e89 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java @@ -27,8 +27,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.TransportPlan; +import org.springframework.format.annotation.DateTimeFormat; import java.io.Serial; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; /** @@ -72,5 +75,24 @@ public class TransportPlanVO extends TransportPlan { @Schema(description = "计划调度生成的运单") private List dispatchRows; + @TableField(exist = false) + @Schema(description = "计划开始日期起") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate planStartDateStart; + + @TableField(exist = false) + @Schema(description = "计划开始日期止") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate planStartDateEnd; + + @TableField(exist = false) + @Schema(description = "创建开始时间") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTimeStart; + + @TableField(exist = false) + @Schema(description = "创建结束时间") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTimeEnd; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java index ff7cacc..ab1bed9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java @@ -119,22 +119,29 @@ public class LoadingManageController extends BladeController { return R.status(loadingManageService.changeRoute(loadingManage)); } - @PostMapping("/cancel") + @PostMapping("/start") @ApiOperationSupport(order = 10) + @Operation(summary = "改为进行中", description = "传入id") + public R start(@Parameter(description = "主键", required = true) @RequestParam Long id) { + return R.status(loadingManageService.start(id)); + } + + @PostMapping("/cancel") + @ApiOperationSupport(order = 11) @Operation(summary = "取消", description = "传入id") public R cancel(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(loadingManageService.cancel(id)); } @PostMapping("/complete") - @ApiOperationSupport(order = 11) + @ApiOperationSupport(order = 12) @Operation(summary = "完成", description = "传入id") public R complete(@Parameter(description = "主键", required = true) @RequestParam Long id) { return R.status(loadingManageService.complete(id)); } @PostMapping("/batch-complete") - @ApiOperationSupport(order = 12) + @ApiOperationSupport(order = 13) @Operation(summary = "批量完成", description = "传入ids") public R batchComplete(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(loadingManageService.batchComplete(ids)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index 1b72c3b..90562fe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -50,6 +50,8 @@ public interface ILoadingManageService extends BaseService { boolean changeRoute(LoadingManage loadingManage); + boolean start(Long id); + boolean cancel(Long id); boolean complete(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 71c9694..dfbd4de 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -29,6 +29,7 @@ import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.wrapper.LoadingManageWrapper; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -40,8 +41,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; /** @@ -76,6 +79,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl entityPage = page(page, buildQuery(loadingManage)); IPage voPage = LoadingManageWrapper.build().pageVO(entityPage); voPage.getRecords().forEach(this::fillReadonly); + fillDriverRejected(voPage.getRecords()); return voPage; } @@ -83,6 +87,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl oldWaybillIds = new ArrayList<>(); + // 新建暂存为草稿;编辑已有单据时保持原业务状态(如待执行),避免被改回草稿 + String businessStatus = STATUS_DRAFT; if (Func.isNotEmpty(loadingManage.getId())) { LoadingManage oldRecord = loadEditable(loadingManage.getId(), true); oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson()); loadingManage.setLoadingNo(oldRecord.getLoadingNo()); loadingManage.setDeptId(oldRecord.getDeptId()); loadingManage.setDeptName(oldRecord.getDeptName()); + if (Func.isNotEmpty(oldRecord.getBusinessStatus())) { + businessStatus = oldRecord.getBusinessStatus(); + } } prepareCreateOrUpdate(loadingManage); - loadingManage.setBusinessStatus(STATUS_DRAFT); + loadingManage.setBusinessStatus(businessStatus); if (Func.isEmpty(loadingManage.getLoadingNo())) { loadingManage.setLoadingNo(nextCode()); } validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson())); validateCarrierContract(loadingManage, false); boolean result = saveOrUpdate(loadingManage); - syncAssociatedWaybills(loadingManage, STATUS_DRAFT, oldWaybillIds); + syncAssociatedWaybills(loadingManage, businessStatus, oldWaybillIds); return result; } @@ -321,6 +331,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson()); @@ -332,6 +345,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl()); + return result; + } + @Override @Transactional(rollbackFor = Exception.class) public boolean cancel(Long id) { @@ -817,6 +844,70 @@ public class LoadingManageServiceImpl extends BaseServiceImpl records) { + if (Func.isEmpty(records)) { + return; + } + List loadingNos = records.stream() + .map(LoadingManage::getLoadingNo) + .filter(Func::isNotEmpty) + .distinct() + .toList(); + Set rejectedLoadingNos = new HashSet<>(); + if (Func.isNotEmpty(loadingNos)) { + rejectedLoadingNos.addAll(waybillMapper.selectList(Wrappers.lambdaQuery() + .select(Waybill::getLoadingNo) + .in(Waybill::getLoadingNo, loadingNos) + .eq(Waybill::getIsDeleted, 0) + .eq(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)) + .stream() + .map(Waybill::getLoadingNo) + .filter(Func::isNotEmpty) + .collect(Collectors.toSet())); + } + for (LoadingManageVO record : records) { + record.setDriverRejected(rejectedLoadingNos.contains(record.getLoadingNo())); + } + } + + private boolean hasDriverRejectedWaybill(LoadingManage loadingManage) { + if (Func.isNotEmpty(loadingManage.getLoadingNo())) { + Long count = waybillMapper.selectCount(Wrappers.lambdaQuery() + .eq(Waybill::getLoadingNo, loadingManage.getLoadingNo()) + .eq(Waybill::getIsDeleted, 0) + .eq(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)); + if (count != null && count > 0) { + return true; + } + } + List ids = waybillIds(loadingManage.getWaybillIdsJson()); + if (Func.isEmpty(ids)) { + return false; + } + Long count = waybillMapper.selectCount(Wrappers.lambdaQuery() + .in(Waybill::getId, ids) + .eq(Waybill::getIsDeleted, 0) + .eq(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)); + return count != null && count > 0; + } + + private void clearAssociatedWaybillAcceptRecords(LoadingManage loadingManage) { + List ids = waybillIds(loadingManage.getWaybillIdsJson()); + var update = Wrappers.lambdaUpdate() + .set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_PENDING) + .set(Waybill::getDriverAcceptTime, null) + .set(Waybill::getDriverAcceptDriverId, null) + .set(Waybill::getDriverRejectTime, null) + .set(Waybill::getDriverRejectReason, null); + if (Func.isNotEmpty(ids)) { + waybillMapper.update(null, update.in(Waybill::getId, ids)); + return; + } + if (Func.isNotEmpty(loadingManage.getLoadingNo())) { + waybillMapper.update(null, update.eq(Waybill::getLoadingNo, loadingManage.getLoadingNo())); + } + } + private String formatMileage(BigDecimal mileage) { if (mileage == null) { return null; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index f71af8d..14468a3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -572,6 +572,6 @@ public class MasterOrderServiceImpl extends BaseServiceImpl } } else { waybill.setLoadingNo(null); + waybill.setMasterNo(null); waybill.setMileageRemark(null); + // 新建(含复制后提交)不沿用源单据状态,与纯新增一致 + waybill.setBusinessStatus(null); } // 无过程快照时按项目回填;再按接单设置决定 pending / running fillProjectProcessConfig(waybill); @@ -717,13 +720,12 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setOtherFeeTotal(source.getOtherFeeTotal()); target.setTaskRemark(source.getTaskRemark()); target.setOriginalNo(source.getOriginalNo()); - target.setBusinessStatus(source.getBusinessStatus()); target.setDataSource(source.getDataSource()); target.setStartDate(source.getStartDate()); target.setEndDate(source.getEndDate()); target.setPlanId(source.getPlanId()); target.setPlanName(source.getPlanName()); - target.setMasterNo(source.getMasterNo()); + target.setMasterNo(null); target.setLoadingNo(null); target.setBatchNo(source.getBatchNo()); target.setRelationNo(source.getRelationNo()); @@ -736,7 +738,8 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setFreightJson(source.getFreightJson()); target.setAttachmentsJson(source.getAttachmentsJson()); target.setRemark(source.getRemark()); - target.setBusinessStatus("pending"); + // 复制后状态与新增一致:先置空,再按过程配置校正为 pending/running + target.setBusinessStatus(null); target.setWaybillNo(nextCode()); clearDriverAcceptRecord(target); fillProjectProcessConfig(target); From 4d412e90eecd631a6fac64eb214fc67bcb43a057 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 04:51:16 +0800 Subject: [PATCH 096/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/granter/IamSsoTokenGranter.java | 34 ++++++++++++++--- .../handler/BladeAuthorizationHandler.java | 6 ++- .../system/service/IUserService.java | 2 +- .../system/service/impl/UserServiceImpl.java | 38 ++++++++++++++++++- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index c1dfecf..1e8a6f0 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -86,6 +86,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { private static final String IAM_TOKEN_URI = "/iam/sso/token"; private static final String BEARER_PREFIX = "Bearer "; private static final String BASIC_PREFIX = "Basic "; + private static final String IAM_DEFAULT_ROLE_ID = "1123598816738675203"; private final OAuth2ClientService clientService; private final IUserClient userClient; @@ -159,27 +160,40 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { if (user == null) { throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); } + if (user.getAuthorities() == null || user.getAuthorities().isEmpty()) { + log.warn("IAM统一身份认证用户缺少可登录角色,tenantId={}, accountNo={}", tenantId, accountNo); + throw new UserInvalidException(OAuth2TokenConstant.USER_HAS_NO_ROLE); + } user.setClient(client(request)); return user; } private UserInfo loadOrCreateIamUser(OAuth2Request request, IamSsoProfileResponse profileResponse, String tenantId, String accountNo) { R result = userClient.userInfo(tenantId, accountNo); - if (result.isSuccess() && hasUser(result.getData())) { + if (result.isSuccess() && hasLoginAccess(result.getData())) { return result.getData(); } - log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}", - tenantId, accountNo, result.isSuccess()); + if (result.isSuccess() && hasUser(result.getData())) { + log.info("IAM统一身份认证账号缺少可登录角色,开始补齐默认角色,tenantId={}, accountNo={}", tenantId, accountNo); + } else { + log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}", + tenantId, accountNo, result.isSuccess()); + } R saveResult = userClient.saveIamUser(buildIamUser(profileResponse, tenantId, accountNo)); if (!saveResult.isSuccess() || !Boolean.TRUE.equals(saveResult.getData())) { - log.warn("IAM统一身份认证自动创建用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, saveResult.getMsg()); - throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); + String failMsg = StringUtil.isNotBlank(saveResult.getMsg()) ? saveResult.getMsg() : OAuth2TokenConstant.USER_HAS_NO_ROLE; + log.warn("IAM统一身份认证自动创建或补齐用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, failMsg); + throw new UserInvalidException(failMsg); } R createdResult = userClient.userInfo(tenantId, accountNo); if (!createdResult.isSuccess() || !hasUser(createdResult.getData())) { log.warn("IAM统一身份认证自动创建用户后未查询到用户,tenantId={}, accountNo={}", tenantId, accountNo); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); } + if (!hasRoles(createdResult.getData())) { + log.warn("IAM统一身份认证自动创建用户后仍缺少可登录角色,tenantId={}, accountNo={}", tenantId, accountNo); + throw new UserInvalidException(OAuth2TokenConstant.USER_HAS_NO_ROLE); + } return createdResult.getData(); } @@ -191,6 +205,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return userInfo != null && userInfo.getUser() != null; } + private boolean hasRoles(UserInfo userInfo) { + return userInfo != null && userInfo.getRoles() != null && !userInfo.getRoles().isEmpty(); + } + + private boolean hasLoginAccess(UserInfo userInfo) { + return hasUser(userInfo) && hasRoles(userInfo); + } + private User buildIamUser(IamSsoProfileResponse profileResponse, String tenantId, String accountNo) { User user = new User(); user.setTenantId(tenantId); @@ -199,7 +221,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { user.setPassword(UUID.randomUUID().toString()); user.setName(accountNo); user.setRealName(accountNo); - user.setRoleId(StringPool.MINUS_ONE); + user.setRoleId(IAM_DEFAULT_ROLE_ID); user.setDeptId(StringPool.MINUS_ONE); user.setPostId(StringPool.MINUS_ONE); user.setIsOa(1); diff --git a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java index e6e793e..2c12576 100644 --- a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java +++ b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java @@ -154,7 +154,11 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler { */ @Override public void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation) { - // 自定义认证失败回调 + log.error("用户:{},认证失败,失败原因:{},grantType={},authorities={}", + user == null ? request.getUsername() : user.getAccount(), + validation.getMessage(), + request.getGrantType(), + user == null ? null : user.getAuthorities()); } /** diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java index e1ff5ee..35899e0 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java @@ -281,7 +281,7 @@ public interface IUserService extends BaseService { boolean registerUser(User user); /** - * 新建IAM统一身份认证用户(按可信租户落库) + * 新建或补齐IAM统一身份认证用户(按可信租户落库,默认分配角色 1123598816738675203) * * @param user 用户实体 * @return 是否成功 diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index 944d99e..8428dfe 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -96,6 +96,7 @@ public class UserServiceImpl extends BaseServiceImpl implement private static final String PASSWORD_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; private static final int RANDOM_PASSWORD_LENGTH = 8; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final Long IAM_DEFAULT_ROLE_ID = 1123598816738675203L; private final IUserDeptService userDeptService; private final UserDataScopeMapper userDataScopeMapper; @@ -563,8 +564,27 @@ public class UserServiceImpl extends BaseServiceImpl implement if (user.getUserType() == null) { user.setUserType(UserType.WEB.getCategory()); } - if (StringUtil.isBlank(user.getRoleId())) { - user.setRoleId(StringPool.MINUS_ONE); + String defaultRoleId = resolveIamDefaultRoleId(); + User existingUser = userByAccount(user.getTenantId(), user.getAccount()); + if (existingUser != null) { + boolean changed = false; + if (isMissingIamRole(existingUser.getRoleId())) { + existingUser.setRoleId(defaultRoleId); + changed = true; + } + if (existingUser.getIsOa() == null || existingUser.getIsOa() != 1) { + existingUser.setIsOa(1); + existingUser.setSyncTime(new Date()); + changed = true; + } + if (!changed) { + return true; + } + CacheUtil.clear(USER_CACHE); + return this.updateById(existingUser); + } + if (isMissingIamRole(user.getRoleId())) { + user.setRoleId(defaultRoleId); } if (StringUtil.isBlank(user.getDeptId())) { user.setDeptId(StringPool.MINUS_ONE); @@ -579,6 +599,20 @@ public class UserServiceImpl extends BaseServiceImpl implement return saveUser(user); } + private boolean isMissingIamRole(String roleId) { + return StringUtil.isBlank(roleId) || StringPool.MINUS_ONE.equals(roleId); + } + + private String resolveIamDefaultRoleId() { + Role role = roleService.getOne(Wrappers.lambdaQuery() + .eq(Role::getId, IAM_DEFAULT_ROLE_ID) + .eq(Role::getIsDeleted, BladeConstant.DB_NOT_DELETED)); + if (role == null || role.getId() == null) { + throw new ServiceException("IAM用户默认角色不存在,roleId=" + IAM_DEFAULT_ROLE_ID); + } + return String.valueOf(role.getId()); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean updatePlatform(Long userId, Integer userType, String userExt) { From d1b4f286b267162027f924d439588c9d3f200f2b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 06:29:26 +0800 Subject: [PATCH 097/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/pom.xml | 4 + .../auth/granter/IamSsoTokenGranter.java | 29 ++- blade-auth/src/main/resources/application.yml | 2 +- .../springblade/system/SystemApplication.java | 4 +- .../system/controller/UserController.java | 11 ++ .../system/props/IamSyncProperties.java | 52 +++++ .../system/service/IUserService.java | 7 + .../system/service/impl/UserServiceImpl.java | 184 +++++++++++++++++- .../src/main/resources/application.yml | 8 + 9 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java diff --git a/blade-auth/pom.xml b/blade-auth/pom.xml index b088727..5640463 100644 --- a/blade-auth/pom.xml +++ b/blade-auth/pom.xml @@ -63,6 +63,10 @@ org.springblade blade-user-api + + org.springblade + blade-dict-api + org.springblade blade-system-api diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index 1e8a6f0..8763a21 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -45,6 +45,7 @@ import org.springblade.core.redis.cache.BladeRedis; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.StringPool; +import org.springblade.system.cache.DictCache; import org.springblade.system.feign.IUserClient; import org.springblade.system.pojo.entity.User; import org.springblade.system.pojo.entity.UserInfo; @@ -86,7 +87,9 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { private static final String IAM_TOKEN_URI = "/iam/sso/token"; private static final String BEARER_PREFIX = "Bearer "; private static final String BASIC_PREFIX = "Basic "; - private static final String IAM_DEFAULT_ROLE_ID = "1123598816738675203"; + private static final String IAM_DEFAULT_DICT_CODE = "iam_default"; + private static final String IAM_DEFAULT_ROLE_NAME = "默认角色"; + private static final String IAM_DEFAULT_DEPT_NAME = "默认部门"; private final OAuth2ClientService clientService; private final IUserClient userClient; @@ -155,6 +158,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { log.warn("IAM统一身份认证请求缺少租户ID,accountNo={}", accountNo); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); } + log.info("IAM统一身份认证开始匹配本系统账号,tenantId={}, accountNo={}", tenantId, accountNo); UserInfo userInfo = loadOrCreateIamUser(request, profileResponse, tenantId, accountNo); OAuth2User user = TokenUtil.convertUser(userInfo, request); if (user == null) { @@ -174,7 +178,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return result.getData(); } if (result.isSuccess() && hasUser(result.getData())) { - log.info("IAM统一身份认证账号缺少可登录角色,开始补齐默认角色,tenantId={}, accountNo={}", tenantId, accountNo); + log.info("IAM统一身份认证账号缺少有效角色或部门,开始补齐默认配置,tenantId={}, accountNo={}", tenantId, accountNo); } else { log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}", tenantId, accountNo, result.isSuccess()); @@ -210,7 +214,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { } private boolean hasLoginAccess(UserInfo userInfo) { - return hasUser(userInfo) && hasRoles(userInfo); + return hasUser(userInfo) && hasRoles(userInfo) && hasValidAssignment(userInfo.getUser()); + } + + private boolean hasValidAssignment(User user) { + return StringUtil.isNotBlank(user.getRoleId()) + && !StringPool.MINUS_ONE.equals(user.getRoleId()) + && StringUtil.isNotBlank(user.getDeptId()) + && !StringPool.MINUS_ONE.equals(user.getDeptId()); } private User buildIamUser(IamSsoProfileResponse profileResponse, String tenantId, String accountNo) { @@ -221,8 +232,8 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { user.setPassword(UUID.randomUUID().toString()); user.setName(accountNo); user.setRealName(accountNo); - user.setRoleId(IAM_DEFAULT_ROLE_ID); - user.setDeptId(StringPool.MINUS_ONE); + user.setRoleId(resolveIamDefaultId(IAM_DEFAULT_ROLE_NAME)); + user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME)); user.setPostId(StringPool.MINUS_ONE); user.setIsOa(1); user.setSyncTime(new Date()); @@ -231,6 +242,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return user; } + private String resolveIamDefaultId(String dictValue) { + String dictKey = DictCache.getKey(IAM_DEFAULT_DICT_CODE, dictValue); + if (StringUtil.isBlank(dictKey) || StringPool.MINUS_ONE.equals(dictKey)) { + throw new UserInvalidException(StringUtil.format("IAM默认配置 [{}] 未配置有效键值", dictValue)); + } + return dictKey; + } + public boolean supports(OAuth2Request request) { return isIamRequest(request, false); } diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index 586debc..714ddbe 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -100,5 +100,5 @@ iam: system-client-id: ${IAM_SSO_SYSTEM_CLIENT_ID:saber3} system-client-secret: ${IAM_SSO_SYSTEM_CLIENT_SECRET:saber3_secret} redirect-uri: ${IAM_SSO_REDIRECT_URI:http://172.16.203.228:8000/callback} - authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + authorization: ${IAM_SSO_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} 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 bbad903..b55719f 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,13 +28,16 @@ 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.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.ComponentScan; +import org.springblade.system.props.IamSyncProperties; /** * 系统模块启动器 * @author Chill */ @ComponentScan(basePackages = {"org.springblade.system", "org.springblade.process"}) +@EnableConfigurationProperties(IamSyncProperties.class) @BladeCloudApplication public class SystemApplication { @@ -44,4 +47,3 @@ public class SystemApplication { } } - diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java index 328fd2a..a4b7b6b 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java @@ -157,6 +157,17 @@ public class UserController { return R.status(userService.submit(user)); } + /** + * 同步IAM账号。 + */ + @IsAdmin + @PostMapping("/sync-iam-accounts") + @ApiOperationSupport(order = 6) + @Operation(summary = "同步IAM账号") + public R syncIamAccounts() { + return R.data(userService.syncIamAccounts()); + } + /** * 修改 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java new file mode 100644 index 0000000..b956961 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java @@ -0,0 +1,52 @@ +/** + * 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.system.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * IAM账号同步配置。 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "iam.sync") +public class IamSyncProperties { + + /** IAM增量账号接口地址。 */ + private String accountListUrl; + + /** IAM接口Authorization请求头。 */ + private String authorization; + + /** IAM接口Auth请求头。 */ + private String profileAuthorization; + + /** 单页请求数量。 */ + private int pageSize = 50; + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java index 35899e0..366db18 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java @@ -78,6 +78,13 @@ public interface IUserService extends BaseService { */ boolean submit(User user); + /** + * 从IAM同步管理租户账号。 + * + * @return 同步处理的账号数量 + */ + int syncIamAccounts(); + /** * 修改用户(租户守卫校验用户归属,含账号 / 手机查重) * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index 8428dfe..3cc6099 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -31,7 +31,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.common.constant.DataStatusEnum; import org.bouncycastle.util.encoders.Hex; import org.springblade.common.constant.ParamConstant; import org.springblade.common.constant.TenantConstant; @@ -63,6 +67,7 @@ import org.springblade.system.pojo.entity.*; import org.springblade.system.pojo.enums.DictEnum; import org.springblade.system.pojo.enums.UserType; import org.springblade.system.pojo.vo.UserVO; +import org.springblade.system.props.IamSyncProperties; import org.springblade.system.service.IRoleService; import org.springblade.system.service.IUserDeptService; import org.springblade.system.service.IUserOauthService; @@ -72,6 +77,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -79,6 +89,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import static org.springblade.common.constant.ParamConstant.DEFAULT_PARAM_PASSWORD; import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE; @@ -91,12 +103,17 @@ import static org.springblade.core.tenant.TenantGuard.EntityType.USER; */ @Service @AllArgsConstructor +@Slf4j public class UserServiceImpl extends BaseServiceImpl implements IUserService { private static final String GUEST_NAME = "guest"; private static final String PASSWORD_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; private static final int RANDOM_PASSWORD_LENGTH = 8; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); - private static final Long IAM_DEFAULT_ROLE_ID = 1123598816738675203L; + private static final String IAM_DEFAULT_DICT_CODE = "iam_default"; + private static final String IAM_DEFAULT_ROLE_NAME = "默认角色"; + private static final String IAM_DEFAULT_DEPT_NAME = "默认部门"; + private static final String IAM_SYNC_TENANT_ID = "000000"; + private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); private final IUserDeptService userDeptService; private final UserDataScopeMapper userDataScopeMapper; @@ -106,6 +123,8 @@ public class UserServiceImpl extends BaseServiceImpl implement private final BladeTenantProperties tenantProperties; private final OAuth2Properties properties; + private final IamSyncProperties iamSyncProperties; + private final ObjectMapper objectMapper; @Override @@ -133,6 +152,147 @@ public class UserServiceImpl extends BaseServiceImpl implement return saveUser(user); } + @Override + @Transactional(rollbackFor = Exception.class) + public int syncIamAccounts() { + int pageNumber = 1; + int fetchedCount = 0; + int syncedCount = 0; + int totalCount = -1; + int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50; + while (true) { + JsonNode dataNode = requestIamAccountPage(pageNumber, pageSize); + JsonNode accountList = dataNode.path("list"); + if (!accountList.isArray() || accountList.isEmpty()) { + break; + } + if (dataNode.has("total")) { + totalCount = dataNode.path("total").asInt(totalCount); + } + for (JsonNode accountNode : accountList) { + if (syncIamAccount(accountNode)) { + syncedCount++; + } + } + fetchedCount += accountList.size(); + int responsePage = dataNode.path("page").asInt(pageNumber); + int responseSize = dataNode.path("size").asInt(pageSize); + if ((totalCount >= 0 && fetchedCount >= totalCount) + || accountList.size() < pageSize + || (totalCount >= 0 && responsePage * responseSize >= totalCount)) { + break; + } + pageNumber = responsePage + 1; + } + log.info("IAM账号同步完成,tenantId={}, fetchedCount={}, syncedCount={}", IAM_SYNC_TENANT_ID, fetchedCount, syncedCount); + return syncedCount; + } + + private JsonNode requestIamAccountPage(int pageNumber, int pageSize) { + try { + Map requestBody = new LinkedHashMap<>(); + requestBody.put("size", String.valueOf(pageSize)); + requestBody.put("page", String.valueOf(pageNumber)); + HttpRequest request = HttpRequest.newBuilder(URI.create(iamSyncProperties.getAccountListUrl())) + .timeout(Duration.ofSeconds(20)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())) + .header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())) + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8)) + .build(); + HttpResponse response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new ServiceException(StringUtil.format("IAM账号接口调用失败,HTTP状态码:{}", response.statusCode())); + } + JsonNode responseNode = objectMapper.readTree(response.body()); + if (!"0".equals(responseNode.path("code").asText())) { + throw new ServiceException(StringUtil.format("IAM账号接口调用失败:{}", responseNode.path("msg").asText())); + } + JsonNode dataNode = responseNode.path("data"); + if (!dataNode.isObject()) { + throw new ServiceException("IAM账号接口返回数据格式错误"); + } + return dataNode; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + log.error("调用IAM账号接口被中断,page={}", pageNumber, exception); + throw new ServiceException("调用IAM账号接口被中断"); + } catch (IOException | IllegalArgumentException exception) { + log.error("调用IAM账号接口失败,page={}", pageNumber, exception); + throw new ServiceException("调用IAM账号接口失败"); + } + } + + private boolean syncIamAccount(JsonNode accountNode) { + String account = readIamText(accountNode, "accountNo"); + if (StringUtil.isBlank(account)) { + log.warn("IAM账号缺少accountNo,跳过同步"); + return false; + } + String name = readIamText(accountNode, "name"); + if (StringUtil.isBlank(name)) { + name = readIamText(accountNode, "accountName"); + } + if (StringUtil.isBlank(name)) { + name = account; + } + Integer status = accountNode.path("status").asInt(0) == 1 + ? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode(); + User user = userByAccount(IAM_SYNC_TENANT_ID, account); + if (user == null) { + user = new User(); + user.setTenantId(IAM_SYNC_TENANT_ID); + user.setAccount(account); + user.setName(name); + user.setRealName(name); + user.setRoleId(resolveIamDefaultRoleId()); + user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME)); + user.setPostId(StringPool.MINUS_ONE); + user.setUserType(UserType.WEB.getCategory()); + user.setStatus(status); + user.setIsOa(1); + user.setSyncTime(new Date()); + applyUserDefaults(user); + return saveUser(user); + } + boolean changed = !Objects.equals(user.getName(), name) || !Objects.equals(user.getRealName(), name) + || !Objects.equals(user.getStatus(), status) || !Objects.equals(user.getIsOa(), 1); + if (isMissingIamRole(user.getRoleId())) { + user.setRoleId(resolveIamDefaultRoleId()); + changed = true; + } + if (isMissingIamAssignment(user.getDeptId())) { + user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME)); + changed = true; + } + if (!changed) { + return true; + } + user.setName(name); + user.setRealName(name); + user.setStatus(status); + user.setIsOa(1); + user.setSyncTime(new Date()); + CacheUtil.clear(USER_CACHE); + return updateById(user); + } + + private String readIamText(JsonNode node, String fieldName) { + JsonNode valueNode = node.get(fieldName); + return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim(); + } + + private String normalizeAuthorizationHeader(String value) { + if (StringUtil.isBlank(value)) { + return StringPool.EMPTY; + } + if (StringUtil.startsWithIgnoreCase(value, "Basic ") || StringUtil.startsWithIgnoreCase(value, "Bearer ")) { + return value; + } + return "Basic " + value; + } + @Override @Transactional(rollbackFor = Exception.class) public boolean updateUser(User user) { @@ -572,6 +732,10 @@ public class UserServiceImpl extends BaseServiceImpl implement existingUser.setRoleId(defaultRoleId); changed = true; } + if (isMissingIamAssignment(existingUser.getDeptId())) { + existingUser.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME)); + changed = true; + } if (existingUser.getIsOa() == null || existingUser.getIsOa() != 1) { existingUser.setIsOa(1); existingUser.setSyncTime(new Date()); @@ -604,13 +768,19 @@ public class UserServiceImpl extends BaseServiceImpl implement } private String resolveIamDefaultRoleId() { - Role role = roleService.getOne(Wrappers.lambdaQuery() - .eq(Role::getId, IAM_DEFAULT_ROLE_ID) - .eq(Role::getIsDeleted, BladeConstant.DB_NOT_DELETED)); - if (role == null || role.getId() == null) { - throw new ServiceException("IAM用户默认角色不存在,roleId=" + IAM_DEFAULT_ROLE_ID); + return resolveIamDefaultId(IAM_DEFAULT_ROLE_NAME); + } + + private String resolveIamDefaultId(String dictValue) { + String dictKey = DictCache.getKey(IAM_DEFAULT_DICT_CODE, dictValue); + if (StringUtil.isBlank(dictKey) || StringPool.MINUS_ONE.equals(dictKey)) { + throw new ServiceException(StringUtil.format("IAM默认配置 [{}] 未配置有效键值", dictValue)); } - return String.valueOf(role.getId()); + return dictKey; + } + + private boolean isMissingIamAssignment(String value) { + return StringUtil.isBlank(value) || StringPool.MINUS_ONE.equals(value); } @Override diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 28509cf..549ebd0 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -23,3 +23,11 @@ spring: url: ${blade.datasource.${spring.profiles.active}.url} username: ${blade.datasource.${spring.profiles.active}.username} password: ${blade.datasource.${spring.profiles.active}.password} + +# IAM账号同步 +iam: + sync: + account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST} + authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50} From 8a79f4d42832b533fe40df3f35aff5fdff4ce579 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 06:36:36 +0800 Subject: [PATCH 098/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index 714ddbe..6edfb41 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -100,5 +100,5 @@ iam: system-client-id: ${IAM_SSO_SYSTEM_CLIENT_ID:saber3} system-client-secret: ${IAM_SSO_SYSTEM_CLIENT_SECRET:saber3_secret} redirect-uri: ${IAM_SSO_REDIRECT_URI:http://172.16.203.228:8000/callback} - authorization: ${IAM_SSO_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} - profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} From b182b700b35cd85e8355a0f24331f4a006ddee21 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 06:52:35 +0800 Subject: [PATCH 099/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/springblade/system/service/impl/UserServiceImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index 3cc6099..c738d73 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -201,6 +201,9 @@ public class UserServiceImpl extends BaseServiceImpl implement .header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())) .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8)) .build(); + log.info("Auth,{}", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())); + log.info("Authorization,{}", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())); + HttpResponse response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new ServiceException(StringUtil.format("IAM账号接口调用失败,HTTP状态码:{}", response.statusCode())); From ad496fdf2840a369c593c07327171e5fc1653c1a Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 07:32:06 +0800 Subject: [PATCH 100/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-service/blade-system/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 549ebd0..f3492fa 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -29,5 +29,5 @@ iam: sync: account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST} authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} - profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50} From cbc7e16453dd46a6eafc8ac508fb53a88d74a80e Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 08:08:09 +0800 Subject: [PATCH 101/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/controller/DeptController.java | 19 +- .../system/props/IamSyncProperties.java | 3 + .../system/service/IDeptService.java | 7 + .../system/service/impl/DeptServiceImpl.java | 195 ++++++++++++++++++ .../system/service/impl/UserServiceImpl.java | 31 ++- .../src/main/resources/application.yml | 1 + doc/nacos/blade-prod.yaml | 9 + 7 files changed, 252 insertions(+), 13 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 11c4081..9cccc5d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -159,12 +159,23 @@ public class DeptController extends BladeController { return R.fail("操作失败"); } + /** + * 同步IAM组织。 + */ + @IsAdmin + @PostMapping("/sync-iam-organizations") + @ApiOperationSupport(order = 7) + @Operation(summary = "同步IAM组织") + public R syncIamOrganizations() { + return R.data(deptService.syncIamOrganizations()); + } + /** * 删除 */ @IsAdmin @PostMapping("/remove") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 8) @Operation(summary = "删除", description = "传入ids") public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { CacheUtil.clear(SYS_CACHE); @@ -177,7 +188,7 @@ public class DeptController extends BladeController { */ @PreAuth(AuthConstant.PERMIT_ALL) @GetMapping("/select") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 9) @Operation(summary = "下拉数据源", description = "传入id集合") public R> select(Long userId, String deptId) { if (Func.isNotEmpty(userId)) { @@ -194,7 +205,7 @@ public class DeptController extends BladeController { */ @PreAuth(AuthConstant.PERMIT_ALL) @GetMapping("/platform-company-select") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 10) @Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表") public R> platformCompanySelect() { return R.data(deptService.listPlatformCompany()); @@ -205,7 +216,7 @@ public class DeptController extends BladeController { */ @IsAdmin @GetMapping("/dept-leader-info") - @ApiOperationSupport(order = 10) + @ApiOperationSupport(order = 11) @Operation(summary = "获取部门的主管信息", description = "传入deptId") public R> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) { List list = deptService.deptLeaderInfo(deptId); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java index b956961..2cbc9a3 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java @@ -40,6 +40,9 @@ public class IamSyncProperties { /** IAM增量账号接口地址。 */ private String accountListUrl; + /** IAM组织接口地址。 */ + private String orgListUrl; + /** IAM接口Authorization请求头。 */ private String authorization; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java index 48e5102..9aef013 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java @@ -156,6 +156,13 @@ public interface IDeptService extends IService { */ boolean submit(Dept dept); + /** + * 从IAM同步管理租户组织。 + + * @return 同步处理的组织数量 + */ + int syncIamOrganizations(); + /** * 按名称与父级查询部门列表(限定当前会话租户) * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java index ff25a47..bf43f44 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java @@ -29,8 +29,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springblade.common.constant.DataStatusEnum; +import org.springblade.core.cache.utils.CacheUtil; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.support.Condition; import org.springblade.core.secure.utils.AuthUtil; @@ -44,6 +48,7 @@ import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.User; import org.springblade.system.pojo.vo.DeptVO; import org.springblade.system.pojo.vo.UserVO; +import org.springblade.system.props.IamSyncProperties; import org.springblade.system.service.IDeptService; import org.springblade.system.service.IUserService; import org.springblade.system.wrapper.DeptWrapper; @@ -52,6 +57,13 @@ import org.springblade.thirdparty.oa.constant.OAConvertConstant; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; @@ -59,6 +71,7 @@ import java.util.stream.Collectors; import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT; import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT_PARENT; +import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE; /** * 服务实现类 @@ -72,8 +85,13 @@ public class DeptServiceImpl extends ServiceImpl implements ID private static final String TENANT_ID = "tenantId"; private static final String PARENT_ID = "parentId"; + private static final String IAM_SYNC_TENANT_ID = "000000"; + private static final Long IAM_SYNC_PARENT_ID = 1123598813738675201L; + private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); private final IUserService userService; + private final IamSyncProperties iamSyncProperties; + private final ObjectMapper objectMapper; @Override public Dept getDetail(Dept dept) { @@ -255,6 +273,183 @@ public class DeptServiceImpl extends ServiceImpl implements ID return saveOrUpdate(dept); } + @Override + @Transactional(rollbackFor = Exception.class) + public int syncIamOrganizations() { + int pageNumber = 1; + int fetchedCount = 0; + int syncedCount = 0; + int totalCount = -1; + int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50; + while (true) { + JsonNode dataNode = requestIamOrgPage(pageNumber, pageSize); + JsonNode orgList = dataNode.path("list"); + if (!orgList.isArray() || orgList.isEmpty()) { + break; + } + if (dataNode.has("total")) { + totalCount = dataNode.path("total").asInt(totalCount); + } + for (JsonNode orgNode : orgList) { + if (syncIamOrganization(orgNode)) { + syncedCount++; + } + } + fetchedCount += orgList.size(); + int responsePage = dataNode.path("page").asInt(pageNumber); + int responseSize = dataNode.path("size").asInt(pageSize); + if ((totalCount >= 0 && fetchedCount >= totalCount) + || orgList.size() < pageSize + || (totalCount >= 0 && responsePage * responseSize >= totalCount)) { + break; + } + pageNumber = responsePage + 1; + } + log.info("IAM组织同步完成,tenantId={}, fetchedCount={}, syncedCount={}", IAM_SYNC_TENANT_ID, fetchedCount, syncedCount); + return syncedCount; + } + + private JsonNode requestIamOrgPage(int pageNumber, int pageSize) { + try { + Map requestBody = new LinkedHashMap<>(); + requestBody.put("size", String.valueOf(pageSize)); + requestBody.put("page", String.valueOf(pageNumber)); + HttpRequest request = HttpRequest.newBuilder(URI.create(iamSyncProperties.getOrgListUrl())) + .timeout(Duration.ofSeconds(20)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())) + .header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())) + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8)) + .build(); + HttpResponse response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new ServiceException(StringUtil.format("IAM组织接口调用失败,HTTP状态码:{}", response.statusCode())); + } + JsonNode responseNode = objectMapper.readTree(response.body()); + if (!"0".equals(responseNode.path("code").asText())) { + throw new ServiceException(StringUtil.format("IAM组织接口调用失败:{}", responseNode.path("msg").asText())); + } + JsonNode dataNode = responseNode.path("data"); + if (!dataNode.isObject()) { + throw new ServiceException("IAM组织接口返回数据格式错误"); + } + return dataNode; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + log.error("调用IAM组织接口被中断,page={}", pageNumber, exception); + throw new ServiceException("调用IAM组织接口被中断"); + } catch (IOException | IllegalArgumentException exception) { + log.error("调用IAM组织接口失败,page={}", pageNumber, exception); + throw new ServiceException("调用IAM组织接口失败"); + } + } + + private boolean syncIamOrganization(JsonNode orgNode) { + String orgCode = readIamText(orgNode, "orgCode", "org_code", "organizationCode", "organization_code", + "code", "app_org__org_code", "app_org__org_no", "app_org__organization_code", "app_org__code"); + String orgId = readIamText(orgNode, "orgId", "org_id", "id", "app_org__id", "app_org__org_id"); + if (StringUtil.isBlank(orgCode)) { + orgCode = orgId; + } + if (StringUtil.isBlank(orgCode)) { + log.warn("IAM组织缺少组织编码,跳过同步"); + return false; + } + if (orgCode.length() > 30) { + log.warn("IAM组织编码超过30个字符,跳过同步,orgCode={}", orgCode); + return false; + } + String orgName = readIamText(orgNode, "name", "orgName", "org_name", "organizationName", "organization_name", + "fullName", "app_org__name", "app_org__org_name", "app_org__org_full_name", "app_org__organization_name"); + if (StringUtil.isBlank(orgName)) { + log.warn("IAM组织缺少组织名称,跳过同步,orgCode={}", orgCode); + return false; + } + Integer status = readIamInt(orgNode, "status", "org_status", "app_org__status", "app_org__org_status") == 1 + ? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode(); + Dept dept = getOne(Wrappers.lambdaQuery() + .eq(Dept::getTenantId, IAM_SYNC_TENANT_ID) + .eq(Dept::getDeptCode, orgCode), false); + if (dept == null) { + dept = new Dept(); + dept.setTenantId(IAM_SYNC_TENANT_ID); + dept.setParentId(IAM_SYNC_PARENT_ID); + dept.setAncestors(resolveIamParentAncestors()); + dept.setDeptCode(orgCode); + dept.setDeptName(orgName); + dept.setFullName(orgName); + dept.setShortName(orgName); + dept.setDeptCategory(1); + dept.setSort(0); + dept.setStatus(status); + dept.setIsDeleted(BladeConstant.DB_NOT_DELETED); + dept.setIsOa(1); + dept.setIsPlatformCompany(0); + dept.setSyncTime(new Date()); + boolean saved = save(dept); + if (saved) { + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + } + return saved; + } + boolean changed = !Objects.equals(dept.getDeptName(), orgName) || !Objects.equals(dept.getFullName(), orgName) + || !Objects.equals(dept.getShortName(), orgName) || !Objects.equals(dept.getStatus(), status) + || !Objects.equals(dept.getIsOa(), 1); + if (!changed) { + return true; + } + dept.setDeptName(orgName); + dept.setFullName(orgName); + dept.setShortName(orgName); + dept.setStatus(status); + dept.setIsOa(1); + dept.setSyncTime(new Date()); + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + return updateById(dept); + } + + private String resolveIamParentAncestors() { + Dept parent = getById(IAM_SYNC_PARENT_ID); + String ancestors = parent == null ? String.valueOf(BladeConstant.TOP_PARENT_ID) : parent.getAncestors(); + if (StringUtil.isBlank(ancestors)) { + ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID); + } + return ancestors + StringPool.COMMA + IAM_SYNC_PARENT_ID; + } + + private String readIamText(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); + return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim(); + } + + private int readIamInt(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); + return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0); + } + + private JsonNode findIamNode(JsonNode node, String... fieldNames) { + for (String fieldName : fieldNames) { + JsonNode valueNode = node.get(fieldName); + if (valueNode != null && !valueNode.isNull()) { + return valueNode; + } + } + return null; + } + + private String normalizeAuthorizationHeader(String value) { + if (StringUtil.isBlank(value)) { + return StringPool.EMPTY; + } + if (StringUtil.startsWithIgnoreCase(value, "Basic ") || StringUtil.startsWithIgnoreCase(value, "Bearer ")) { + return value; + } + return "Basic " + value; + } + private void validateDeptCategory(Dept dept, Dept parent) { if (parent == null) { throw new ServiceException("请选择上级组织"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index c738d73..3eb7d43 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -200,9 +200,7 @@ public class UserServiceImpl extends BaseServiceImpl implement .header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())) .header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())) .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8)) - .build(); - log.info("Auth,{}", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())); - log.info("Authorization,{}", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())); + .build(); HttpResponse response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() < 200 || response.statusCode() >= 300) { @@ -228,19 +226,19 @@ public class UserServiceImpl extends BaseServiceImpl implement } private boolean syncIamAccount(JsonNode accountNode) { - String account = readIamText(accountNode, "accountNo"); + String account = readIamText(accountNode, "accountNo", "account_no", "app_account__account_no"); if (StringUtil.isBlank(account)) { log.warn("IAM账号缺少accountNo,跳过同步"); return false; } - String name = readIamText(accountNode, "name"); + String name = readIamText(accountNode, "name", "app_account__name"); if (StringUtil.isBlank(name)) { - name = readIamText(accountNode, "accountName"); + name = readIamText(accountNode, "accountName", "account_name", "app_account__account_name"); } if (StringUtil.isBlank(name)) { name = account; } - Integer status = accountNode.path("status").asInt(0) == 1 + Integer status = readIamInt(accountNode, "status", "app_account__status") == 1 ? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode(); User user = userByAccount(IAM_SYNC_TENANT_ID, account); if (user == null) { @@ -281,11 +279,26 @@ public class UserServiceImpl extends BaseServiceImpl implement return updateById(user); } - private String readIamText(JsonNode node, String fieldName) { - JsonNode valueNode = node.get(fieldName); + private String readIamText(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim(); } + private int readIamInt(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); + return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0); + } + + private JsonNode findIamNode(JsonNode node, String... fieldNames) { + for (String fieldName : fieldNames) { + JsonNode valueNode = node.get(fieldName); + if (valueNode != null && !valueNode.isNull()) { + return valueNode; + } + } + return null; + } + private String normalizeAuthorizationHeader(String value) { if (StringUtil.isBlank(value)) { return StringPool.EMPTY; diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index f3492fa..1d8d630 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -28,6 +28,7 @@ spring: iam: sync: account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST} + org-list-url: ${IAM_SSO_ORG_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ORG_LIST} authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50} diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 16c1b02..7a749a0 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -107,3 +107,12 @@ baidu: powerjob: worker: server-address: 172.16.203.228:7700 + +# IAM账号与组织同步配置 +iam: + sync: + account-list-url: http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST + org-list-url: http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ORG_LIST + authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} + page-size: 50 From 8207afe04f72d97c86a1486344c27b259bd21742 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 08:50:24 +0800 Subject: [PATCH 102/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/granter/IamSsoTokenGranter.java | 27 ++++++++++++------- .../auth/props/IamSsoProperties.java | 3 ++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index 8763a21..e7c6a27 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -85,7 +85,6 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { private static final String GRANT_TYPE = "iam_sso"; private static final String IAM_GRANT_TYPE = "authorization_code"; private static final String IAM_TOKEN_URI = "/iam/sso/token"; - private static final String BEARER_PREFIX = "Bearer "; private static final String BASIC_PREFIX = "Basic "; private static final String IAM_DEFAULT_DICT_CODE = "iam_default"; private static final String IAM_DEFAULT_ROLE_NAME = "默认角色"; @@ -339,12 +338,16 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { private IamSsoProfileResponse requestIamProfile(String accessToken) { try { - HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(buildProfileUrl(accessToken))) + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(buildProfileUrl(accessToken))) .timeout(Duration.ofSeconds(10)) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .header(HttpHeaders.AUTHORIZATION, profileAuthorizationHeader(accessToken)) - .GET() - .build(); + // 与 IAM 同步接口一致:Authorization 走网关凭证,Auth 走业务凭证。 + .header(HttpHeaders.AUTHORIZATION, authorizationHeader()); + String profileAuthHeader = profileAuthHeader(); + if (StringUtil.isNotBlank(profileAuthHeader)) { + requestBuilder.header("Auth", profileAuthHeader); + } + HttpRequest httpRequest = requestBuilder.GET().build(); log.info("IAM统一身份认证获取用户信息请求,url={}, method={}", properties.getProfileUrl(), httpRequest.method()); HttpResponse response = httpClient.send( httpRequest, @@ -352,7 +355,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { ); log.info("IAM统一身份认证获取用户信息响应,status={}", response.statusCode()); if (response.statusCode() < 200 || response.statusCode() >= 300) { - log.warn("IAM统一身份认证获取用户信息失败,status={}", response.statusCode()); + log.warn("IAM统一身份认证获取用户信息失败,status={}, body={}", response.statusCode(), response.body()); throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT); } return objectMapper.readValue(response.body(), IamSsoProfileResponse.class); @@ -420,11 +423,15 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return BASIC_PREFIX + authorization; } - private String profileAuthorizationHeader(String accessToken) { - if (StringUtil.isNotBlank(properties.getProfileAuthorization())) { - return withBasicPrefix(properties.getProfileAuthorization()); + /** + * IAM 业务侧 Auth 头。网关 Authorization 使用 {@link #authorizationHeader()}, + * 用户令牌通过 query 参数 access_token 传递。 + */ + private String profileAuthHeader() { + if (StringUtil.isBlank(properties.getProfileAuthorization())) { + return null; } - return BEARER_PREFIX + accessToken; + return withBasicPrefix(properties.getProfileAuthorization()); } private String encode(String value) { diff --git a/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java b/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java index ff74ff2..8515859 100644 --- a/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java +++ b/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java @@ -78,7 +78,8 @@ public class IamSsoProperties { private String authorization; /** - * IAM用户信息认证头。为空时默认使用 Bearer accessToken + * IAM业务侧 Auth 请求头。为空时不传 Auth; + * Authorization 统一使用网关凭证 {@link #authorization}。 */ private String profileAuthorization; From 0fa0eae43cac42694ae0f0bdfe55746aa51dc0a3 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 23:42:01 +0800 Subject: [PATCH 103/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E8=BF=90=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/WaybillServiceImpl.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 208d411..14e3c3b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -477,9 +477,11 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setLoadingNo(null); waybill.setMasterNo(null); waybill.setMileageRemark(null); - // 新建(含复制后提交)不沿用源单据状态,与纯新增一致 - waybill.setBusinessStatus(null); } + // 新建若已带业务状态(如复制提交)则在过程校正后回写,保证与原运单一致 + String preservedBusinessStatus = created + ? TransportBusinessSupport.trimToNull(waybill.getBusinessStatus()) + : null; // 无过程快照时按项目回填;再按接单设置决定 pending / running fillProjectProcessConfig(waybill); prepare(waybill); @@ -489,6 +491,9 @@ public class WaybillServiceImpl extends BaseServiceImpl clearDriverAcceptRecord(waybill); } applyDriverAcceptBusinessStatus(waybill); + if (Func.isNotEmpty(preservedBusinessStatus)) { + waybill.setBusinessStatus(preservedBusinessStatus); + } fillCustomerName(waybill); if (created && Func.isEmpty(waybill.getWaybillNo())) { waybill.setWaybillNo(nextCode()); @@ -738,13 +743,17 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setFreightJson(source.getFreightJson()); target.setAttachmentsJson(source.getAttachmentsJson()); target.setRemark(source.getRemark()); - // 复制后状态与新增一致:先置空,再按过程配置校正为 pending/running - target.setBusinessStatus(null); + // 复制后业务状态与原运单保持一致 + String sourceBusinessStatus = source.getBusinessStatus(); + target.setBusinessStatus(sourceBusinessStatus); target.setWaybillNo(nextCode()); clearDriverAcceptRecord(target); fillProjectProcessConfig(target); prepare(target); applyDriverAcceptBusinessStatus(target); + if (Func.isNotEmpty(sourceBusinessStatus)) { + target.setBusinessStatus(sourceBusinessStatus); + } validate(target); save(target); return detail(target.getId()); From 01993779a7adb587a732b03da21f7f3a10a2f16a Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 18 Sep 2026 16:31:37 +0800 Subject: [PATCH 104/114] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=202?= =?UTF-8?q?=E3=80=81=E8=B0=83=E6=95=B4OA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blade-auth/pom.xml | 4 + .../auth/constant/BladeAuthConstant.java | 6 + .../OAuth2UserInfoResponseAdvice.java | 94 ++ .../auth/endpoint/OAuth2UserInfoVO.java | 55 + .../auth/endpoint/Oauth2SmsEndpoint.java | 15 +- .../auth/granter/SmsTokenGranter.java | 9 +- .../auth/granter/WechatMiniTokenGranter.java | 102 ++ .../service/BladeClientDetailService.java | 15 +- .../gateway/provider/AuthProvider.java | 1 + .../springblade/system/feign/ISysClient.java | 10 + .../system/feign/ISysClientFallback.java | 5 + .../system/pojo/vo/OaPersonSyncPageVO.java | 66 + .../pojo/vo/AdminDriverOptionVO.java | 30 + .../transport/pojo/vo/AdminHomeBadgesVO.java | 47 + .../transport/pojo/vo/AdminHomeStatsVO.java | 53 + .../transport/pojo/vo/AdminHomeVO.java | 55 + .../transport/pojo/vo/AdminTodoItemVO.java | 65 + .../pojo/vo/AdminVehicleOptionVO.java | 24 + .../transport/pojo/vo/AdminWaybillCardVO.java | 95 ++ .../pojo/vo/AdminWaybillDetailVO.java | 154 ++ .../pojo/vo/DriverWaybillCardVO.java | 51 + .../springblade/system/feign/IUserClient.java | 15 + .../system/pojo/dto/PhoneChangeDTO.java | 54 + .../system/pojo/dto/PhoneVerifyDTO.java | 51 + blade-service/blade-system/pom.xml | 4 + .../system/controller/UserController.java | 27 +- .../controller/UserPhoneController.java | 93 ++ .../system/convert/UserConvert.java | 28 +- .../springblade/system/feign/SysClient.java | 6 + .../springblade/system/feign/UserClient.java | 6 + .../system/service/IMenuService.java | 8 + .../system/service/IOASyncService.java | 18 + .../system/service/IUserPhoneService.java | 63 + .../system/service/IUserService.java | 5 + .../system/service/impl/MenuServiceImpl.java | 29 + .../service/impl/OASyncServiceImpl.java | 293 ++-- .../service/impl/OaUserListSyncHelper.java | 609 ++++++++ .../service/impl/UserPhoneServiceImpl.java | 212 +++ .../system/service/impl/UserServiceImpl.java | 40 + .../ExceptionDisposalController.java | 12 +- .../controller/ManageWaybillController.java | 143 ++ .../service/IDriverWaybillService.java | 6 + .../service/IManageWaybillService.java | 90 ++ .../transport/service/IWaybillService.java | 6 + .../impl/DriverWaybillServiceImpl.java | 74 +- .../impl/ManageWaybillServiceImpl.java | 642 ++++++++ .../service/impl/WaybillServiceImpl.java | 12 +- .../oa/config/OAFeignClientConfig.java | 23 +- .../oa/constant/OAConvertConstant.java | 4 + .../thirdparty/oa/feign/IOAClient.java | 2 +- .../blade-wechat-api/pom.xml | 27 + .../ThirdPartyWechatAutoConfiguration.java | 14 + .../wechat/config/WechatMiniProperties.java | 28 + .../wechat/constant/WechatMiniConstant.java | 20 + .../wechat/exception/WechatMiniException.java | 16 + .../wechat/pojo/vo/WechatPhoneVO.java | 25 + .../wechat/pojo/vo/WechatSessionVO.java | 21 + .../wechat/service/IWechatMiniService.java | 21 + .../service/impl/WechatMiniServiceImpl.java | 148 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + blade-third-party-api/pom.xml | 1 + doc/nacos/blade-dev.yaml | 6 + doc/nacos/blade-prod.yaml | 5 + doc/nacos/blade.yaml | 3 + doc/nacos/routes/blade-gateway-dev.json | 22 + doc/nacos/third-party-api.yaml | 6 + doc/sql/update/add-mp-auth-permission.sql | 21 + .../update/add-wechat-applet-grant-type.sql | 8 + hs_err_pid22077.log | 1379 +++++++++++++++++ hs_err_pid40988.log | 1368 ++++++++++++++++ pom.xml | 5 + 71 files changed, 6463 insertions(+), 213 deletions(-) create mode 100644 blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java create mode 100644 blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoVO.java create mode 100644 blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java create mode 100644 blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminDriverOptionVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeBadgesVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeStatsVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminTodoItemVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminVehicleOptionVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java create mode 100644 blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneChangeDTO.java create mode 100644 blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneVerifyDTO.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/service/IUserPhoneService.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IManageWaybillService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java create mode 100644 blade-third-party-api/blade-wechat-api/pom.xml create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/ThirdPartyWechatAutoConfiguration.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/WechatMiniProperties.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/constant/WechatMiniConstant.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/exception/WechatMiniException.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatPhoneVO.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatSessionVO.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/IWechatMiniService.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/impl/WechatMiniServiceImpl.java create mode 100644 blade-third-party-api/blade-wechat-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 doc/sql/update/add-mp-auth-permission.sql create mode 100644 doc/sql/update/add-wechat-applet-grant-type.sql create mode 100644 hs_err_pid22077.log create mode 100644 hs_err_pid40988.log diff --git a/blade-auth/pom.xml b/blade-auth/pom.xml index 5640463..8bfe4ea 100644 --- a/blade-auth/pom.xml +++ b/blade-auth/pom.xml @@ -75,6 +75,10 @@ org.springblade blade-mk-api + + org.springblade + blade-wechat-api + org.springblade blade-resource-api diff --git a/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java b/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java index b9c8420..b8f8ed3 100644 --- a/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java +++ b/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java @@ -32,4 +32,10 @@ package org.springblade.auth.constant; */ public interface BladeAuthConstant { + /** + * 小程序/登录短信验证码资源编号(对应后台 /resource/sms 的 smsCode) + */ + String LOGIN_SMS_CODE = "ali_reg"; + } + diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java new file mode 100644 index 0000000..8e207ad --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java @@ -0,0 +1,94 @@ +/** + * 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.auth.endpoint; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.feign.ISysClient; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.Collections; +import java.util.List; + +/** + * 增强 /oauth/user-info 响应,补充 permission 权限标识字段 + * + * @author Chill + */ +@Slf4j +@RequiredArgsConstructor +@RestControllerAdvice(assignableTypes = OAuth2TokenEndPoint.class) +public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice { + + private final ISysClient sysClient; + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return returnType.getMethod() != null && "userInfo".equals(returnType.getMethod().getName()); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (!(body instanceof BladeUser bladeUser)) { + return body; + } + OAuth2UserInfoVO userInfo = BeanUtil.copyProperties(bladeUser, OAuth2UserInfoVO.class); + if (userInfo == null) { + return body; + } + userInfo.setPermission(loadPermission(bladeUser.getRoleId())); + return userInfo; + } + + private List loadPermission(String roleId) { + if (Func.isBlank(roleId)) { + return Collections.emptyList(); + } + try { + R> result = sysClient.getPermissions(roleId); + if (result != null && result.isSuccess() && result.getData() != null) { + return result.getData(); + } + } catch (Exception exception) { + log.warn("加载用户权限标识失败, roleId={}", roleId, exception); + } + return Collections.emptyList(); + } + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoVO.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoVO.java new file mode 100644 index 0000000..528a490 --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoVO.java @@ -0,0 +1,55 @@ +/** + * 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.auth.endpoint; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.secure.BladeUser; + +import java.io.Serial; +import java.util.List; + +/** + * OAuth用户信息(含权限标识,供小程序等客户端使用) + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "OAuth用户信息") +public class OAuth2UserInfoVO extends BladeUser { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 权限标识集合(菜单按钮 code) + */ + @Schema(description = "权限标识集合") + private List permission; + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java index 878c526..b48ac80 100644 --- a/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java @@ -28,13 +28,14 @@ package org.springblade.auth.endpoint; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import lombok.SneakyThrows; +import org.springblade.auth.constant.BladeAuthConstant; import org.springblade.core.oauth2.props.OAuth2Properties; import org.springblade.core.oauth2.provider.OAuth2Request; import org.springblade.core.oauth2.service.OAuth2User; import org.springblade.core.oauth2.service.OAuth2UserService; import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.SM2Util; -import org.springblade.core.tool.utils.StringPool; import org.springblade.core.tool.utils.StringUtil; import org.springblade.resource.feign.ISmsClient; import org.springblade.resource.utils.SmsUtil; @@ -73,11 +74,14 @@ public class Oauth2SmsEndpoint { * 短信验证码发送 * * @param tenantId 租户ID - * @param phone 手机号 + * @param phone 手机号(SM2 加密) + * @param code 短信资源编号,默认 ali_reg(后台 /resource/sms) */ @SneakyThrows @PostMapping("/oauth/sms/send-validate") - public R sendValidate(@RequestParam String tenantId, @RequestParam String phone) { + public R sendValidate(@RequestParam String tenantId, + @RequestParam String phone, + @RequestParam(required = false) String code) { // 校验手机加密认证,防止恶意发送验证码 String decryptedPhone = SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey()); if (StringUtil.isBlank(decryptedPhone)) { @@ -90,8 +94,9 @@ public class Oauth2SmsEndpoint { if (oAuth2User == null) { return R.fail(USER_PHONE_NOT_FOUND); } - // 用户存在则发送验证码 - R result = smsClient.sendValidate(tenantId, StringPool.EMPTY, decryptedPhone); + // 使用指定短信资源(默认 ali_reg) + String smsResourceCode = Func.toStr(code, BladeAuthConstant.LOGIN_SMS_CODE); + R result = smsClient.sendValidate(tenantId, smsResourceCode, decryptedPhone); return result.isSuccess() ? R.data(result.getData(), SmsUtil.SEND_SUCCESS) : R.fail(SmsUtil.SEND_FAIL); } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java index 0c37eee..540095c 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java @@ -26,6 +26,7 @@ package org.springblade.auth.granter; import jakarta.servlet.http.HttpServletRequest; +import org.springblade.auth.constant.BladeAuthConstant; import org.springblade.core.oauth2.constant.OAuth2TokenConstant; import org.springblade.core.oauth2.exception.UserInvalidException; import org.springblade.core.oauth2.granter.AbstractTokenGranter; @@ -37,8 +38,8 @@ import org.springblade.core.oauth2.service.OAuth2User; import org.springblade.core.oauth2.service.OAuth2UserService; import org.springblade.core.sms.model.SmsCode; import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.SM2Util; -import org.springblade.core.tool.utils.StringPool; import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.WebUtil; import org.springblade.resource.feign.ISmsClient; @@ -78,8 +79,10 @@ public class SmsTokenGranter extends AbstractTokenGranter { if (StringUtil.isBlank(decryptedPhone)) { throw new UserInvalidException(OAuth2TokenConstant.USER_PHONE_NOT_FOUND); } - // 获取短信验证信息 - R result = smsClient.validateMessage(tenantId, StringPool.EMPTY, smsCode.getId(), smsCode.getValue(), decryptedPhone); + // 与发送时使用同一短信资源编号(默认 ali_reg) + HttpServletRequest httpRequest = WebUtil.getRequest(); + String smsResourceCode = Func.toStr(httpRequest.getParameter("code"), BladeAuthConstant.LOGIN_SMS_CODE); + R result = smsClient.validateMessage(tenantId, smsResourceCode, smsCode.getId(), smsCode.getValue(), decryptedPhone); if (!result.isSuccess()) { throw new UserInvalidException(OAuth2TokenConstant.CAPTCHA_NOT_CORRECT); } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java new file mode 100644 index 0000000..1bdd67b --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java @@ -0,0 +1,102 @@ +package org.springblade.auth.granter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.oauth2.exception.UserInvalidException; +import org.springblade.core.oauth2.granter.AbstractTokenGranter; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springblade.system.feign.IUserClient; +import org.springblade.thirdparty.wechat.constant.WechatMiniConstant; +import org.springblade.thirdparty.wechat.exception.WechatMiniException; +import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO; +import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO; +import org.springblade.thirdparty.wechat.service.IWechatMiniService; +import org.springframework.stereotype.Component; + +/** + * 微信小程序手机号一键登录。 + *

+ * grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。 + * 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。 + */ +@Slf4j +@Component +public class WechatMiniTokenGranter extends AbstractTokenGranter { + + private final OAuth2UserService userService; + private final IWechatMiniService wechatMiniService; + private final IUserClient userClient; + + public WechatMiniTokenGranter(OAuth2ClientService clientService, + OAuth2UserService userService, + PasswordHandler passwordHandler, + IWechatMiniService wechatMiniService, + IUserClient userClient) { + super(clientService, userService, passwordHandler); + this.userService = userService; + this.wechatMiniService = wechatMiniService; + this.userClient = userClient; + } + + @Override + public String type() { + return WechatMiniConstant.GRANT_TYPE; + } + + @Override + public OAuth2User user(OAuth2Request request) { + // 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败 + var oauthClient = client(request); + + HttpServletRequest httpRequest = WebUtil.getRequest(); + String loginCode = httpRequest.getParameter("loginCode"); + String phoneCode = httpRequest.getParameter("phoneCode"); + if (StringUtil.isBlank(loginCode) || StringUtil.isBlank(phoneCode)) { + throw new UserInvalidException("微信登录参数不完整"); + } + + WechatSessionVO session; + WechatPhoneVO phoneInfo; + try { + session = wechatMiniService.code2Session(loginCode); + phoneInfo = wechatMiniService.getPhoneNumber(phoneCode); + } catch (WechatMiniException e) { + log.warn("微信小程序登录失败: {}", e.getMessage()); + throw new UserInvalidException(e.getMessage()); + } + + String phone = Func.toStr(phoneInfo.getPurePhoneNumber(), phoneInfo.getPhoneNumber()); + if (StringUtil.isBlank(phone)) { + throw new UserInvalidException("未获取到微信手机号"); + } + + OAuth2User user = userService.loadByPhone(phone, request); + if (!userService.validateUser(user)) { + throw new UserInvalidException("用户不存在,无法登录"); + } + + R bindResult = userClient.bindWxMiniOpenId( + request.getTenantId(), + Func.toLong(user.getUserId()), + session.getOpenid(), + phone + ); + if (bindResult == null || !bindResult.isSuccess()) { + String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败"; + log.warn("绑定微信 openid 失败 userId={} openid={} msg={}", user.getUserId(), session.getOpenid(), msg); + throw new UserInvalidException(StringUtil.isBlank(msg) ? "绑定 openid 失败" : msg); + } + + user.setClient(oauthClient); + return user; + } + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java b/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java index ee43e92..db727e8 100644 --- a/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java +++ b/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java @@ -25,11 +25,17 @@ */ package org.springblade.auth.service; +import org.springblade.core.oauth2.constant.OAuth2GranterConstant; import org.springblade.core.oauth2.provider.OAuth2Request; import org.springblade.core.oauth2.service.OAuth2Client; import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; import org.springframework.jdbc.core.JdbcTemplate; +import java.util.Arrays; +import java.util.Optional; + /** * BladeClientDetailService * @@ -57,6 +63,13 @@ public class BladeClientDetailService extends OAuth2ClientDetailService { @Override public boolean validateGranter(OAuth2Client client, String grantType) { - return super.validateGranter(client, grantType); + // 微信小程序一键登录:兼容库表未配置 wechat_applet 的存量客户端 + if (OAuth2GranterConstant.WECHAT_APPLET.equals(grantType) || "wechat_mini".equals(grantType)) { + return true; + } + return Optional.ofNullable(client) + .map(c -> Arrays.stream(Func.split(c.getAuthorizedGrantTypes(), StringPool.COMMA)) + .anyMatch(s -> s.trim().equals(grantType))) + .orElse(false); } } 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 86787cf..a259f7b 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 @@ -48,6 +48,7 @@ public class AuthProvider { DEFAULT_SKIP_URL.add("/oauth/sms/**"); DEFAULT_SKIP_URL.add("/oauth/clear-cache/**"); DEFAULT_SKIP_URL.add("/oauth/user-info"); + DEFAULT_SKIP_URL.add("/oauth/logout/**"); DEFAULT_SKIP_URL.add("/oauth/render/**"); DEFAULT_SKIP_URL.add("/oauth/callback/**"); DEFAULT_SKIP_URL.add("/oauth/revoke/**"); diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java index 37e33b1..77eeef8 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java @@ -73,6 +73,7 @@ public interface ISysClient { String REGION = API_PREFIX + "/region"; String FEE_ITEMS = API_PREFIX + "/fee-items"; String CARGO_TYPES = API_PREFIX + "/cargo-types"; + String PERMISSIONS = API_PREFIX + "/permissions"; /** * 获取菜单 @@ -316,4 +317,13 @@ public interface ISysClient { @GetMapping(CARGO_TYPES) R> getCargoTypes(); + /** + * 获取角色权限标识集合(按钮编号) + * + * @param roleId 角色id + * @return 权限标识 + */ + @GetMapping(PERMISSIONS) + R> getPermissions(@RequestParam("roleId") String roleId); + } diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java index c7598c3..4825a4d 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java @@ -174,4 +174,9 @@ public class ISysClientFallback implements ISysClient { return R.fail("获取数据失败"); } + @Override + public R> getPermissions(String roleId) { + return R.fail("获取数据失败"); + } + } diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java new file mode 100644 index 0000000..b493bdf --- /dev/null +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java @@ -0,0 +1,66 @@ +/** + * 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.system.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * OA人员分页同步结果 + * + * @author Chill + */ +@Data +@Schema(description = "OA人员分页同步结果") +public class OaPersonSyncPageVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "当前页") + private Integer current; + + @Schema(description = "每页条数") + private Integer size; + + @Schema(description = "OA人员总条数") + private Long total; + + @Schema(description = "本页从OA拉取的条数") + private Integer fetchedCount; + + @Schema(description = "本页同步成功条数") + private Integer syncedCount; + + @Schema(description = "本页跳过条数") + private Integer skippedCount; + + @Schema(description = "是否已到最后一页") + private Boolean finished; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminDriverOptionVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminDriverOptionVO.java new file mode 100644 index 0000000..21839e5 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminDriverOptionVO.java @@ -0,0 +1,30 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Schema(description = "调度端司机搜索项") +public class AdminDriverOptionVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "司机ID") + private Long id; + + @Schema(description = "姓名") + private String name; + + @Schema(description = "手机号") + private String phone; + + @Schema(description = "绑定车牌") + private String vehicleNo; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeBadgesVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeBadgesVO.java new file mode 100644 index 0000000..d37bd5b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeBadgesVO.java @@ -0,0 +1,47 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 调度端首页快捷入口角标 + */ +@Data +@Schema(description = "调度端首页角标") +public class AdminHomeBadgesVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "异常处置待办数(disposalStatus≠completed)") + private long exception; + + @Schema(description = "风险记录待办数(disposalStatus=pending)") + private long risk; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeStatsVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeStatsVO.java new file mode 100644 index 0000000..b3b9770 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeStatsVO.java @@ -0,0 +1,53 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 调度端首页顶部统计(对齐小程序 admin/home) + */ +@Data +@Schema(description = "调度端首页运单状态统计") +public class AdminHomeStatsVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运输中(businessStatus=running)") + private long transporting; + + @Schema(description = "待接单(businessStatus=pending)") + private long pendingAccept; + + @Schema(description = "在途异常(异常处置状态≠已完成)") + private long exception; + + @Schema(description = "已完成(businessStatus=completed)") + private long completed; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeVO.java new file mode 100644 index 0000000..2302825 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeVO.java @@ -0,0 +1,55 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 调度端首页聚合数据(统计 + 角标 + 待处理 feed + 用户名) + */ +@Data +@Schema(description = "调度端首页聚合") +public class AdminHomeVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "当前登录用户姓名") + private String userName; + + @Schema(description = "顶部运单状态统计") + private AdminHomeStatsVO stats = new AdminHomeStatsVO(); + + @Schema(description = "快捷入口角标") + private AdminHomeBadgesVO badges = new AdminHomeBadgesVO(); + + @Schema(description = "待处理事项(异常处置状态≠已完成)") + private List feed = new ArrayList<>(); + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminTodoItemVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminTodoItemVO.java new file mode 100644 index 0000000..8d8430f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminTodoItemVO.java @@ -0,0 +1,65 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 调度端首页「待处理事项」条目 + */ +@Data +@Schema(description = "调度端首页待处理事项") +public class AdminTodoItemVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "条目ID(异常处置ID)") + private Long id; + + @Schema(description = "类型:exception=异常待处置 / reassign=重新派单待处理") + private String type; + + @Schema(description = "标题") + private String title; + + @Schema(description = "相对时间文案,如「2分钟」") + private String timeAgo; + + @Schema(description = "摘要描述") + private String desc; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "操作按钮文案") + private String actionLabel; + + @Schema(description = "跳转路径(小程序内路径)") + private String targetUrl; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminVehicleOptionVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminVehicleOptionVO.java new file mode 100644 index 0000000..53bcaa4 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminVehicleOptionVO.java @@ -0,0 +1,24 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Schema(description = "调度端车牌搜索项") +public class AdminVehicleOptionVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "关联司机名(可选)") + private String driverName; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java new file mode 100644 index 0000000..9e63df3 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java @@ -0,0 +1,95 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 调度端运单列表卡片(对齐小程序 admin/waybill-list) + */ +@Data +@Schema(description = "调度端运单列表卡片") +public class AdminWaybillCardVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long id; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "起点") + private String fromName; + + @Schema(description = "终点") + private String toName; + + @Schema(description = "货物名称") + private String cargo; + + @Schema(description = "重量(带单位)") + private String weight; + + @Schema(description = "计划开始时间") + private String planTime; + + @Schema(description = "计划结束时间") + private String planTimeEnd; + + @Schema(description = "状态:0待接单/1运输中/2已完成/3已取消") + private Integer status; + + @Schema(description = "承运方") + private String carrierName; + + @Schema(description = "司机姓名") + private String driverName; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "是否有未完成异常") + private Boolean hasException; + + @Schema(description = "运输组织类型:common普通 / load配载") + private String transportType; + + @Schema(description = "运输方式:road / 公路运输 / 铁路运输 等") + private String transportMode; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "需重新派单(司机已拒单)") + private Boolean needReassign; + + @Schema(description = "采购方是否已付款(预留)") + private Boolean buyerPaid; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java new file mode 100644 index 0000000..ebbd9cc --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java @@ -0,0 +1,154 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 调度端运单详情(对齐小程序 pages/waybill/detail) + */ +@Data +@Schema(description = "调度端运单详情") +public class AdminWaybillDetailVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long id; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "状态:0待接单/1运输中/2已完成/3已取消") + private Integer status; + + @Schema(description = "起点名称") + private String fromName; + + @Schema(description = "终点名称") + private String toName; + + @Schema(description = "起点地址") + private String fromAddress; + + @Schema(description = "终点地址") + private String toAddress; + + @Schema(description = "装货地址") + private String pickupAddress; + + @Schema(description = "卸货地址") + private String unloadAddress; + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "货物数量(带单位)") + private String cargoQuantity; + + @Schema(description = "重量(带单位)") + private String weight; + + @Schema(description = "合计货重") + private String totalWeight; + + @Schema(description = "运输方式文案:公路运输 / 铁路运输 等") + private String transportType; + + @Schema(description = "运输方式字典值:road 等") + private String transportMode; + + @Schema(description = "运输组织:common普通 / load配载") + private String transportOrgType; + + @Schema(description = "计划发货时间") + private String planShipTime; + + @Schema(description = "计划完成时间") + private String planFinishTime; + + @Schema(description = "承运方") + private String carrierName; + + @Schema(description = "司机姓名") + private String driverName; + + @Schema(description = "司机联系方式") + private String driverPhone; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否有未完成异常") + private Boolean hasException; + + @Schema(description = "最近一条未完成异常处置ID(有异常时返回)") + private Long exceptionId; + + @Schema(description = "需重新派单") + private Boolean needReassign; + + @Schema(description = "司机接单状态:pending/accepted/rejected") + private String acceptStatus; + + @Schema(description = "司机拒绝接单原因") + private String rejectReason; + + @Schema(description = "原指派司机ID") + private Long driverId; + + @Schema(description = "装卸点列表") + private List routePoints; + + @Schema(description = "过程打卡节点(与司机端 punchNodes 同结构)") + private List punchNodes; + + @Schema(description = "途打卡记录") + private List enrouteRecords; + + @Data + @Schema(description = "装卸点") + public static class AdminRoutePointVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "点位名称") + private String name; + + @Schema(description = "详细地址") + private String address; + + @Schema(description = "状态:pending/done/active") + private String status; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java index 162b386..57e394d 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java @@ -133,4 +133,55 @@ public class DriverWaybillCardVO implements Serializable { @Schema(description = "过程配置中 punch=是 的打卡节点列表(详情返回)") private List punchNodes; + /* ===== pages/waybill/detail 接单查看字段 ===== */ + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "装货地址") + private String pickupAddress; + + @Schema(description = "卸货地址") + private String unloadAddress; + + @Schema(description = "货物数量(带单位)") + private String cargoQuantity; + + @Schema(description = "运输方式文案:公路运输等") + private String transportType; + + @Schema(description = "计划发货时间") + private String planShipTime; + + @Schema(description = "计划完成时间") + private String planFinishTime; + + @Schema(description = "合计货重") + private String totalWeight; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "装卸点(详情返回)") + private List routePoints; + + @Schema(description = "过程配置 JSON(详情返回,供前端兜底推导打卡节点)") + private String processJson; + + @Data + @Schema(description = "司机端装卸点") + public static class DriverRoutePointVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "点位名称") + private String name; + + @Schema(description = "详细地址") + private String address; + + @Schema(description = "状态:pending/done/active") + private String status; + } + } diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java index cc78a95..50be69e 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java @@ -61,6 +61,7 @@ public interface IUserClient { String SAVE_IAM_USER = API_PREFIX + "/save-iam-user"; String REGISTER_USER = API_PREFIX + "/register-user"; String REMOVE_USER = API_PREFIX + "/remove-user"; + String BIND_WX_MINI_OPENID = API_PREFIX + "/bind-wx-mini-openid"; /** * 获取用户信息 @@ -187,4 +188,18 @@ public interface IUserClient { @PostMapping(REMOVE_USER) R removeUser(@RequestParam("tenantIds") String tenantIds); + /** + * 绑定微信小程序 openid(写入 blade_user_oauth,source=WECHAT_MINI) + * + * @param tenantId 租户ID + * @param userId 用户ID + * @param openid 微信 openid + * @param phone 手机号(可选,写入 username) + */ + @PostMapping(BIND_WX_MINI_OPENID) + R bindWxMiniOpenId(@RequestParam("tenantId") String tenantId, + @RequestParam("userId") Long userId, + @RequestParam("openid") String openid, + @RequestParam(value = "phone", required = false) String phone); + } diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneChangeDTO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneChangeDTO.java new file mode 100644 index 0000000..1743376 --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneChangeDTO.java @@ -0,0 +1,54 @@ +/** + * 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.system.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 更换手机号参数 + * + * @author Chill + */ +@Data +@Schema(description = "更换手机号参数") +public class PhoneChangeDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "新手机号", requiredMode = Schema.RequiredMode.REQUIRED) + private String newPhone; + + @Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "新手机号短信验证码", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneVerifyDTO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneVerifyDTO.java new file mode 100644 index 0000000..79520b8 --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneVerifyDTO.java @@ -0,0 +1,51 @@ +/** + * 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.system.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 原手机号短信校验参数 + * + * @author Chill + */ +@Data +@Schema(description = "原手机号短信校验参数") +public class PhoneVerifyDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; +} diff --git a/blade-service/blade-system/pom.xml b/blade-service/blade-system/pom.xml index 08aea8b..35b4f52 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-resource-api + org.springblade blade-process-api diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java index a4b7b6b..36c7f7d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java @@ -54,7 +54,9 @@ import org.springblade.core.tool.utils.StringPool; import org.springblade.system.excel.UserExcel; import org.springblade.system.excel.UserImporter; import org.springblade.system.pojo.entity.User; +import org.springblade.system.pojo.vo.OaPersonSyncPageVO; import org.springblade.system.pojo.vo.UserVO; +import org.springblade.system.service.IOASyncService; import org.springblade.system.service.IUserService; import org.springblade.system.wrapper.UserWrapper; import org.springframework.web.bind.annotation.*; @@ -77,6 +79,7 @@ import java.util.Map; public class UserController { private final IUserService userService; + private final IOASyncService oaSyncService; /** * 查询单条 @@ -158,14 +161,16 @@ public class UserController { } /** - * 同步IAM账号。 + * 从OA按页同步人员,并按公司/部门生成组织后绑定到三级部门。 */ @IsAdmin @PostMapping("/sync-iam-accounts") @ApiOperationSupport(order = 6) - @Operation(summary = "同步IAM账号") - public R syncIamAccounts() { - return R.data(userService.syncIamAccounts()); + @Operation(summary = "同步OA人员") + public R syncIamAccounts( + @RequestParam(defaultValue = "1") Integer current, + @RequestParam(defaultValue = "50") Integer size) { + return R.data(oaSyncService.syncPersonFromUserList(current, size)); } /** @@ -227,6 +232,20 @@ public class UserController { return R.status(temp); } + /** + * 当前用户设置/重置登录密码(小程序首次设密、短信验证后改密) + *

+ * 对外路径:/blade-system/user/password ;网关别名 /blade-user/password 亦可到达。 + */ + @PostMapping("/password") + @ApiOperationSupport(order = 10) + @Operation(summary = "设置登录密码", description = "当前登录用户设置密码,无需原密码") + public R password(BladeUser user, + @Parameter(description = "新密码", required = true) @RequestParam String password, + @Parameter(description = "确认密码", required = true) @RequestParam String password2) { + return R.status(userService.setPassword(user.getUserId(), password, password2)); + } + /** * 管理员修改密码 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java new file mode 100644 index 0000000..ecf7b71 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java @@ -0,0 +1,93 @@ +/** + * 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.system.controller; + +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springblade.core.tenant.annotation.NonDS; +import org.springblade.core.tool.api.R; +import org.springblade.system.pojo.dto.PhoneChangeDTO; +import org.springblade.system.pojo.dto.PhoneVerifyDTO; +import org.springblade.system.service.IUserPhoneService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户手机号变更(小程序「修改手机号」) + *

+ * 对外路径:/blade-system/user/phone/** ;网关别名 /blade-user/phone/** 亦可到达。 + * + * @author Chill + */ +@NonDS +@RestController +@AllArgsConstructor +@RequestMapping("/user/phone") +@Tag(name = "用户手机号", description = "修改手机号") +public class UserPhoneController { + + private final IUserPhoneService userPhoneService; + + /** + * 发送短信验证码(需登录) + *

+ * 当前手机号、未占用的新手机号均可发送;新号若已被其他账号占用则拒绝。 + */ + @PostMapping("/send-code") + @ApiOperationSupport(order = 1) + @Operation(summary = "发送手机号变更验证码", description = "传入明文手机号,返回短信校验 id") + public R sendCode(@Parameter(description = "手机号", required = true) @RequestParam String phone) { + return userPhoneService.sendCode(phone); + } + + /** + * 校验原手机号验证码(修改手机号第 1 步) + */ + @PostMapping("/verify-old") + @ApiOperationSupport(order = 2) + @Operation(summary = "校验原手机号验证码", description = "传入发送验证码返回的 id 与验证码") + public R verifyOld(@Valid @RequestBody PhoneVerifyDTO phoneVerify) { + return R.status(userPhoneService.verifyOldPhone(phoneVerify)); + } + + /** + * 绑定新手机号(修改手机号第 3 步,需先完成 verify-old) + */ + @PostMapping("/change") + @ApiOperationSupport(order = 3) + @Operation(summary = "更换手机号", description = "传入新手机号及短信校验 id、验证码") + public R change(@Valid @RequestBody PhoneChangeDTO phoneChange) { + return R.status(userPhoneService.changePhone(phoneChange)); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java index 924daee..28f7004 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java @@ -64,11 +64,11 @@ public interface UserConvert { @Mapping(target = "password", ignore = true) @Mapping(target = "birthday", ignore = true) @Mapping(target = "sex", ignore = true) + @Mapping(target = "account", ignore = true) @Mapping(source = "workcode", target = "code") @Mapping(source = "lastname", target = "name") @Mapping(source = "lastname", target = "realName") @Mapping(source = "mobile", target = "phone") - @Mapping(source = "mobile", target = "account") @Mapping(source = "email", target = "email") User baseConvert(OAPersonResponse person); @@ -86,7 +86,7 @@ public interface UserConvert { */ default User person2user(OAPersonResponse person, String defaultPassword) { User user = baseConvert(person); - + user.setAccount(resolveAccount(person)); // 密码 user.setPassword(defaultPassword); // 性别 @@ -102,6 +102,28 @@ public interface UserConvert { return user; } + /** + * 解析本系统登录账号:优先 OA loginid,其次工号,最后手机号 + * + * @param person OA人员 + * @return 账号,无法识别时返回 null + */ + default String resolveAccount(OAPersonResponse person) { + if (person == null) { + return null; + } + if (StringUtils.isNotBlank(person.getLoginid())) { + return person.getLoginid().trim(); + } + if (StringUtils.isNotBlank(person.getWorkcode())) { + return person.getWorkcode().trim(); + } + if (StringUtils.isNotBlank(person.getMobile())) { + return person.getMobile().trim(); + } + return null; + } + /** * oa人员转本系统用户部门 * @param person @@ -120,7 +142,7 @@ public interface UserConvert { // 排序 userDept.setSort(OAUtils.parseInt(person.getDsporder())); // 用户id - userDept.setUserId(userMap.get(person.getMobile())); + userDept.setUserId(userMap.get(resolveAccount(person))); userDept.setSyncTime(new Date()); return userDept; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java index 49c8613..d08d36a 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java @@ -231,4 +231,10 @@ public class SysClient implements ISysClient { .orderByAsc(CargoType::getCargoCode))); } + @Override + @GetMapping(PERMISSIONS) + public R> getPermissions(String roleId) { + return R.data(menuService.permissionCodes(roleId)); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java index 12b97fd..f25da25 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java @@ -133,4 +133,10 @@ public class UserClient implements IUserClient { return R.data(service.remove(Wrappers.query().lambda().in(User::getTenantId, Func.toStrList(tenantIds)))); } + @Override + @PostMapping(BIND_WX_MINI_OPENID) + public R bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) { + return R.data(service.bindWxMiniOpenId(tenantId, userId, openid, phone)); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java index 6b18066..2d7c77f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMenuService.java @@ -77,6 +77,14 @@ public interface IMenuService extends IService

{ */ List buttons(String roleId); + /** + * 权限标识集合(按钮编号,与前端 GetButtons 叶子 code 一致) + * + * @param roleId 角色id + * @return 权限标识 + */ + List permissionCodes(String roleId); + /** * 树形结构 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java index 1c7c1d9..45b7437 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java @@ -1,5 +1,7 @@ package org.springblade.system.service; +import org.springblade.system.pojo.vo.OaPersonSyncPageVO; + /** * oa同步接口 * @author bfhuange @@ -18,4 +20,20 @@ public interface IOASyncService { * @param syncAll 是否同步所有 */ void syncPersonAndPushMK(boolean syncAll); + + /** + * 从 OA 人员接口全量同步组织与人员,不推送 MK + * + * @return 处理的人员数量 + */ + int syncPersonFromUserList(); + + /** + * 按页从 OA 人员接口同步组织与人员 + * + * @param current 当前页,从 1 开始 + * @param size 每页条数 + * @return 本页同步结果 + */ + OaPersonSyncPageVO syncPersonFromUserList(int current, int size); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserPhoneService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserPhoneService.java new file mode 100644 index 0000000..75653ac --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserPhoneService.java @@ -0,0 +1,63 @@ +/** + * 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.system.service; + +import org.springblade.core.tool.api.R; +import org.springblade.system.pojo.dto.PhoneChangeDTO; +import org.springblade.system.pojo.dto.PhoneVerifyDTO; + +/** + * 用户手机号变更服务 + * + * @author Chill + */ +public interface IUserPhoneService { + + /** + * 发送变更手机号短信验证码 + * + * @param phone 明文手机号 + * @return 含短信校验 id 的响应 + */ + R sendCode(String phone); + + /** + * 校验原手机号验证码,通过后写入短期凭证 + * + * @param phoneVerify 校验参数 + * @return 是否通过 + */ + boolean verifyOldPhone(PhoneVerifyDTO phoneVerify); + + /** + * 校验新手机号验证码并更换手机号 + * + * @param phoneChange 更换参数 + * @return 是否成功 + */ + boolean changePhone(PhoneChangeDTO phoneChange); + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java index 366db18..7c3db61 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java @@ -188,6 +188,11 @@ public interface IUserService extends BaseService { */ UserInfo userInfo(UserOauth userOauth); + /** + * 绑定微信小程序 openid 到已有用户(blade_user_oauth,source=WECHAT_MINI) + */ + boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone); + /** * 根据租户与账号获取用户 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java index e04feb2..7b0ced4 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MenuServiceImpl.java @@ -164,6 +164,35 @@ public class MenuServiceImpl extends ServiceImpl implements IM return menuWrapper.listNodeVO(buttons); } + @Override + public List permissionCodes(String roleId) { + List permissionCodes = new ArrayList<>(); + // Feign 调用时无登录态,不能走 AuthUtil.isAdministrator() 分支;按 roleId 取按钮权限 + List

buttons = StringUtil.isBlank(roleId) + ? Collections.emptyList() + : baseMapper.buttons(Func.toLongList(roleId)); + MenuWrapper menuWrapper = new MenuWrapper(); + collectLeafPermissionCodes(menuWrapper.listNodeVO(buttons), permissionCodes); + return permissionCodes; + } + + /** + * 递归收集按钮树叶子节点的权限编号(与前端 SET_PERMISSION 逻辑一致) + */ + private void collectLeafPermissionCodes(List menuList, List permissionCodes) { + if (menuList == null || menuList.isEmpty()) { + return; + } + for (MenuVO menu : menuList) { + List children = menu.getChildren(); + if (children != null && !children.isEmpty()) { + collectLeafPermissionCodes(children, permissionCodes); + } else if (StringUtil.isNotBlank(menu.getCode())) { + permissionCodes.add(menu.getCode()); + } + } + } + @Override public List tree() { return ForestNodeMerger.merge(baseMapper.tree()); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 38e7eb9..6e0c3e9 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -11,16 +11,14 @@ import org.springblade.common.constant.DictTypeEnum; import org.springblade.core.cache.utils.CacheUtil; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.tool.utils.DateUtil; -import org.springblade.core.tool.utils.DigestUtil; import org.springblade.system.cache.DictCache; import org.springblade.system.cache.ParamCache; import org.springblade.system.convert.DeptConvert; -import org.springblade.system.convert.UserConvert; import org.springblade.system.log.ComposeLogUtil; import org.springblade.system.pojo.entity.*; import org.springblade.system.pojo.enums.DataSync; import org.springblade.system.pojo.enums.DeptCategory; -import org.springblade.system.pojo.vo.UserDeptIdsVO; +import org.springblade.system.pojo.vo.OaPersonSyncPageVO; import org.springblade.system.service.*; import org.springblade.system.util.DataSyncRecordUtils; import org.springblade.thirdparty.oa.constant.OAConstant; @@ -29,6 +27,8 @@ import org.springblade.thirdparty.oa.feign.IOAClient; import org.springblade.thirdparty.oa.pojo.response.OACompanyResponse; import org.springblade.thirdparty.oa.pojo.response.OADepartmentResponse; import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse; +import org.springblade.thirdparty.oa.pojo.response.OAResponse; +import org.springblade.thirdparty.oa.pojo.response.OAResponseData; import org.springblade.thirdparty.oa.pojo.search.OACompanySearch; import org.springblade.thirdparty.oa.pojo.search.OADepartmentSearch; import org.springblade.thirdparty.oa.pojo.search.OAPersonSearch; @@ -38,6 +38,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -54,20 +55,14 @@ import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE; @RequiredArgsConstructor @Service public class OASyncServiceImpl implements IOASyncService { - /** - * 用户表部门id最大长度 - */ - private static final int MAX_DEPT_ID_LENGTH = 2000; private final IOAClient oaClient; private final DeptConvert deptConvert; private final IDeptService deptService; - private final IUserService userService; - private final UserConvert userConvert; - private final IUserDeptService userDeptService; private final IRoleService roleService; private final IMKPushService mkPushService; private final IDataSyncRecordService dataSyncRecordService; + private final OaUserListSyncHelper oaUserListSyncHelper; @Transactional(rollbackFor = Exception.class) @Override @@ -104,6 +99,31 @@ public class OASyncServiceImpl implements IOASyncService { } } + @Transactional(rollbackFor = Exception.class) + @Override + public int syncPersonFromUserList() { + AtomicInteger syncedCount = new AtomicInteger(); + try { + ComposeLogUtil.addLog(log); + this.syncAndRecord(DataSyncRecordUtils::createOAPersonFetch, startTime -> + syncedCount.set(this.syncPersonFromOa(null)), true); + return syncedCount.get(); + } finally { + ComposeLogUtil.removeLastLog(); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public OaPersonSyncPageVO syncPersonFromUserList(int current, int size) { + try { + ComposeLogUtil.addLog(log); + return this.syncPersonFromOaPage(current, size); + } finally { + ComposeLogUtil.removeLastLog(); + } + } + /** * 同步并记录 * @@ -213,174 +233,93 @@ public class OASyncServiceImpl implements IOASyncService { * @param startTime 查询开始时间 */ private void syncPerson(Date startTime) { - String subCompanyIds = getSubCompanyIds(); - if (StringUtils.isEmpty(subCompanyIds)) { - ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数"); - return; - } - // 1. 设置查询参数 - OAPersonSearch personSearch = new OAPersonSearch(); - personSearch.setCurPage(1); - personSearch.setSubcompanyid1(subCompanyIds); - if (startTime != null) { - // 开始时间不为空,设置修改时间参数 - personSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); - } - // 2. 分页查询并处理数据 - OAUtils.pageSyncHandler(personSearch, param -> oaClient.queryPersonPage(new OASearch<>(param)), response -> { - ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(response)); - return new ServiceException("调用OA接口查询人员信息失败"); - }, 10000, ComposeLogUtil.getLastLog()::info).accept(this::handlePerson); - // 清除用户缓存 - CacheUtil.clear(USER_CACHE); + this.syncPersonFromOa(startTime); } /** - * 处理oa人员 - * @param oaPersons + * 从 OA 人员列表同步组织与人员 + * + * @param startTime 增量查询开始时间,为空则全量 + * @return 处理的人员数量 */ - private void handlePerson(List oaPersons) { - if (CollectionUtil.isEmpty(oaPersons)) { - // 数据为空,直接返回 - return; - } - // 根公司id - String rootCompanyId = getRootCompanyId(); - if (rootCompanyId == null) { - return; - } - // 根公司下要同步的部门id - Set rootCompanyDeptIds = getRootCompanyDeptIds(rootCompanyId); - oaPersons = oaPersons.stream() - // 公司不是根公司,或者部门在根公司下要同步的部门列表中 - .filter(oaPerson -> !rootCompanyId.equals(oaPerson.getSubcompanyid1()) || rootCompanyDeptIds.contains(oaPerson.getDepartmentid())) - .toList(); - // 根据手机号转成set - TreeSet oaPersonSet = CollectionUtil.toTreeSet(oaPersons, Comparator.comparing(OAPersonResponse::getMobile)); - // 默认密码 - String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD)); - // 转换数据 - List users = oaPersonSet.stream() - // 只需要手机不为空的 - .filter(person -> StringUtils.isNotBlank(person.getMobile())) - .map(person -> userConvert.person2user(person, defaultPassword)) - .toList(); - List phones = users.stream() - .map(User::getPhone) - .filter(StringUtils::isNotBlank) - .distinct() - .toList(); - // 查询所有用户 - Map userMap = userService.list(Wrappers.lambdaQuery() - //.eq(User::getIsDeleted, BladeConstant.DB_NOT_DELETED) - .in(User::getPhone, phones) - ).stream() - // 解密手机号 - .peek(userService::decryptPhone) - .collect(Collectors.toMap(User::getPhone, User::getId, (a, b) -> b)); - // 数据库存在的所有用户id - Set existsUserIds = new HashSet<>(userMap.values()); - users.forEach(user -> { - if (userMap.containsKey(user.getPhone())) { - // 根据手机号获取对应的用户id - user.setId(userMap.get(user.getPhone())); - // 清空密码,不修改密码 - user.setPassword(null); - } else { - // 没有就生成一个id - user.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); - userService.encryptPhone(user); - userMap.put(user.getPhone(), user.getId()); - } - }); + private int syncPersonFromOa(Date startTime) { + OAPersonSearch personSearch = buildPersonSearch(startTime); + List oaPersons = new ArrayList<>(); + OAUtils.pageSyncHandler(personSearch, param -> oaClient.queryPersonPage(new OASearch<>(param)), response -> { + ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(response)); + return new ServiceException("调用OA接口查询人员信息失败"); + }, 10000, ComposeLogUtil.getLastLog()::info).accept(oaPersons::addAll); + OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons); + OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex); + CacheUtil.clear(USER_CACHE); + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + return personSyncCount.getSyncedCount(); + } - // 不存在的新增 - List addUsers = users.stream() - .filter(user -> !existsUserIds.contains(user.getId())) - .toList(); - if (CollectionUtil.isNotEmpty(addUsers)) { - ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size()); - userService.saveBatch(addUsers); + /** + * 按页从 OA 人员列表同步组织与人员 + * + * @param current 当前页 + * @param size 每页条数 + * @return 本页同步结果 + */ + private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size) { + int pageNo = current < 1 ? 1 : current; + int pageSize = size < 1 ? 50 : Math.min(size, 200); + OAPersonSearch personSearch = buildPersonSearch(null); + personSearch.setCurPage(pageNo); + personSearch.setPageSize(pageSize); + OAResponse oaResponse = oaClient.queryPersonPage(new OASearch<>(personSearch)); + if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) { + ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(oaResponse)); + throw new ServiceException("调用OA接口查询人员信息失败"); } - // 存在的修改 - List updateUsers = users.stream() - .filter(user -> existsUserIds.contains(user.getId())) - .toList(); - if (CollectionUtil.isNotEmpty(updateUsers)) { - ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size()); - userService.updateBatchById(updateUsers); - } - // 没有手机号的数据 = 手机号为空的数量 - long noPhoneNum = oaPersons.stream() - .map(OAPersonResponse::getMobile) - .filter(StringUtils::isBlank) - .count(); - ComposeLogUtil.getLastLog().info("没有手机号的数据:{}", noPhoneNum); + OAResponseData responseData = oaResponse.getData(); + List oaPersons = responseData.getDataList() == null + ? Collections.emptyList() : responseData.getDataList(); + long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize(); + OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons); + OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex); + CacheUtil.clear(USER_CACHE); + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + OaPersonSyncPageVO pageVO = new OaPersonSyncPageVO(); + pageVO.setCurrent(pageNo); + pageVO.setSize(pageSize); + pageVO.setTotal(totalSize); + pageVO.setFetchedCount(oaPersons.size()); + pageVO.setSyncedCount(personSyncCount.getSyncedCount()); + pageVO.setSkippedCount(personSyncCount.getSkippedCount()); + boolean finished = oaPersons.isEmpty() + || oaPersons.size() < pageSize + || (long) pageNo * pageSize >= totalSize; + pageVO.setFinished(finished); + ComposeLogUtil.getLastLog().info("OA人员分页同步完成 {}/{},成功{},跳过{}", + pageNo, totalSize, personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount()); + return pageVO; + } - Map userDeptMap = userDeptService.list(Wrappers.lambdaQuery() - .in(UserDept::getUserId, userMap.values()) - ).stream() - .collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (a, b) -> b)); - // 数据库存在的所有用户部门id - Set existsUserDeptIds = new HashSet<>(userDeptMap.values()); - - List userDeptList = oaPersons.stream() - // 只要包含用户手机号的 - .filter(oaPerson -> userMap.containsKey(oaPerson.getMobile())) - .map(oaPerson -> userConvert.person2userDept(oaPerson, userMap)) - .toList(); - userDeptList.forEach(userDept -> { - String userDeptKey = getUserDeptKey(userDept); - if (userDeptMap.containsKey(userDeptKey)) { - // 根据key获取用户部门id - userDept.setId(userDeptMap.get(userDeptKey)); - } else { - // 没有就生成一个id - userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); - } - }); - // 不存在的新增 - List addList = userDeptList.stream() - .filter(userDept -> !existsUserDeptIds.contains(userDept.getId())) - .toList(); - if (CollectionUtil.isNotEmpty(addList)) { - ComposeLogUtil.getLastLog().info("批量新增用户部门:{}", addList.size()); - userDeptService.saveBatch(addList); - } - // 存在的修改 - List updateList = userDeptList.stream() - .filter(userDept -> existsUserDeptIds.contains(userDept.getId())) - .toList(); - if (CollectionUtil.isNotEmpty(updateList)) { - ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size()); - userDeptService.updateBatchById(updateList); - } - // 回写部门id到用户表 - Collection userIds = userMap.values(); - List list = userDeptService.queryUserDeptIds(userIds); - // 查询没有角色的用户id - Set noRoleUserIds = userService.list(Wrappers.lambdaQuery() - .in(User::getId, userIds) - .isNull(User::getRoleId) - ).stream() - .map(User::getId) - .collect(Collectors.toSet()); - // 获取默认角色id - String defaultRoleId = getDefaultRoleId(); - List updateUserParams = list.stream() - // 过滤掉空部门id及长度超长的 - .filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH) - .map(userDeptIds -> { - User user = new User(); - user.setId(userDeptIds.getUserId()); - user.setDeptId(userDeptIds.getDeptIds()); - user.setDeptCodes(userDeptIds.getDeptCodes()); - if (noRoleUserIds.contains(userDeptIds.getUserId())) { - user.setRoleId(defaultRoleId); - } - return user; - }).toList(); - userService.updateBatchById(updateUserParams); + /** + * 组装 OA 人员分页查询参数 + * + * @param startTime 增量查询开始时间 + * @return 查询参数 + */ + private OAPersonSearch buildPersonSearch(Date startTime) { + OAPersonSearch personSearch = new OAPersonSearch(); + personSearch.setCurPage(1); + personSearch.setPageSize(200); + personSearch.setCreated(""); + personSearch.setWorkcode(""); + personSearch.setSubcompanyid1(""); + personSearch.setDepartmentid(""); + personSearch.setJobtitleid(""); + personSearch.setId(""); + personSearch.setLoginid(""); + personSearch.setIsadaccount(""); + personSearch.setModified(startTime == null ? "" : DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); + return personSearch; } /** @@ -634,16 +573,4 @@ public class OASyncServiceImpl implements IOASyncService { // 部门编码去掉前缀,就是oa的id return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, ""); } - - /** - * 获取用户部门唯一标识,用户id+公司编码+部门编码 - * @param userDept - * @return - */ - private String getUserDeptKey(UserDept userDept) { - if (userDept == null) { - return null; - } - return userDept.getUserId() + userDept.getCompanyCode() + userDept.getDeptCode(); - } } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java new file mode 100644 index 0000000..6a8a188 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java @@ -0,0 +1,609 @@ +/** + * 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.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springblade.common.constant.DataStatusEnum; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.DigestUtil; +import org.springblade.system.cache.ParamCache; +import org.springblade.system.convert.UserConvert; +import org.springblade.system.log.ComposeLogUtil; +import org.springblade.system.pojo.entity.Dept; +import org.springblade.system.pojo.entity.Role; +import org.springblade.system.pojo.entity.User; +import org.springblade.system.pojo.entity.UserDept; +import org.springblade.system.pojo.enums.DeptCategory; +import org.springblade.system.pojo.vo.UserDeptIdsVO; +import org.springblade.system.service.IDeptService; +import org.springblade.system.service.IRoleService; +import org.springblade.system.service.IUserDeptService; +import org.springblade.system.service.IUserService; +import org.springblade.thirdparty.oa.constant.OAConvertConstant; +import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_PASSWORD; +import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_ROLE; +import static org.springblade.common.constant.CommonConstant.DEFAULT_ROLE; +import static org.springblade.common.constant.CommonConstant.YES; + +/** + * 从 OA 人员列表提取组织并同步人员 + * + * @author Chill + */ +@Component +@RequiredArgsConstructor +public class OaUserListSyncHelper { + + private static final int MAX_DEPT_ID_LENGTH = 2000; + + private final IDeptService deptService; + private final IUserService userService; + private final UserConvert userConvert; + private final IUserDeptService userDeptService; + private final IRoleService roleService; + + /** + * 从人员数据提取二级公司、三级部门,并挂到「桂物物流集团」下 + * + * @param oaPersons OA人员 + * @return 组织索引 + */ + public OaOrgIndex syncOrgsFromPersons(List oaPersons) { + OaOrgIndex orgIndex = new OaOrgIndex(); + if (CollectionUtil.isEmpty(oaPersons)) { + return orgIndex; + } + Date orgSyncStart = new Date(); + Dept rootCompany = this.getOrCreateRootCompany(); + String tenantId = resolveTenantId(); + Map companyPersonMap = new LinkedHashMap<>(); + Map departmentPersonMap = new LinkedHashMap<>(); + for (OAPersonResponse oaPerson : oaPersons) { + String companyKey = resolveCompanyKey(oaPerson); + if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getSubcompanyname())) { + companyPersonMap.putIfAbsent(companyKey, oaPerson); + } + String departmentKey = resolveDepartmentKey(oaPerson); + if (StringUtils.isNotBlank(departmentKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname()) + && StringUtils.isNotBlank(oaPerson.getSubcompanyname())) { + departmentPersonMap.putIfAbsent(departmentKey, oaPerson); + } + } + + Map existingCompanyByCode = new HashMap<>(); + Map existingCompanyByName = new HashMap<>(); + deptService.list(Wrappers.lambdaQuery() + .eq(Dept::getParentId, rootCompany.getId()) + .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode()) + ).forEach(dept -> { + if (StringUtils.isNotBlank(dept.getDeptCode())) { + existingCompanyByCode.put(dept.getDeptCode(), dept); + } + if (StringUtils.isNotBlank(dept.getDeptName())) { + existingCompanyByName.put(dept.getDeptName(), dept); + } + }); + List addCompanies = new ArrayList<>(); + List updateCompanies = new ArrayList<>(); + companyPersonMap.forEach((companyKey, oaPerson) -> { + String oaCode = buildCompanyCode(oaPerson, companyKey); + Dept existing = existingCompanyByCode.get(oaCode); + if (existing == null) { + existing = existingCompanyByName.get(oaPerson.getSubcompanyname()); + } + Dept company = this.upsertOrg(oaPerson.getSubcompanyname(), oaCode, existing, rootCompany, tenantId, + DeptCategory.COMPANY, addCompanies, updateCompanies); + orgIndex.companyByOaId.put(companyKey, company); + }); + this.saveOrgs(addCompanies, updateCompanies, DeptCategory.COMPANY); + + Map existingDeptByCode = new HashMap<>(); + Map existingDeptByParentAndName = new HashMap<>(); + List companyIds = orgIndex.companyByOaId.values().stream() + .map(Dept::getId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (CollectionUtil.isNotEmpty(companyIds)) { + deptService.list(Wrappers.lambdaQuery() + .in(Dept::getParentId, companyIds) + .eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode()) + ).forEach(dept -> { + if (StringUtils.isNotBlank(dept.getDeptCode())) { + existingDeptByCode.put(dept.getDeptCode(), dept); + } + if (dept.getParentId() != null && StringUtils.isNotBlank(dept.getDeptName())) { + existingDeptByParentAndName.put(dept.getParentId() + "#" + dept.getDeptName(), dept); + } + }); + } + List addDepartments = new ArrayList<>(); + List updateDepartments = new ArrayList<>(); + departmentPersonMap.forEach((departmentKey, oaPerson) -> { + Dept parentCompany = orgIndex.findCompany(oaPerson); + if (parentCompany == null || parentCompany.getId() == null) { + return; + } + String oaCode = buildDepartmentCode(oaPerson, departmentKey); + Dept existing = existingDeptByCode.get(oaCode); + if (existing == null) { + existing = existingDeptByParentAndName.get(parentCompany.getId() + "#" + oaPerson.getDepartmentname()); + } + Dept department = this.upsertOrg(oaPerson.getDepartmentname(), oaCode, existing, parentCompany, tenantId, + DeptCategory.DEPT, addDepartments, updateDepartments); + orgIndex.deptByOaId.put(departmentKey, department); + orgIndex.deptByCompanyAndName.put(resolveCompanyKey(oaPerson) + "#" + oaPerson.getDepartmentname(), department); + }); + this.saveOrgs(addDepartments, updateDepartments, DeptCategory.DEPT); + deptService.updateAncestors(orgSyncStart); + ComposeLogUtil.getLastLog().info("同步人员提取组织完成,二级公司{}个,三级部门{}个", + orgIndex.companyByOaId.size(), orgIndex.deptByOaId.size()); + return orgIndex; + } + + /** + * 同步人员并绑定到三级部门 + * + * @param oaPersons OA人员 + * @param orgIndex 组织索引 + * @return 本批同步成功与跳过数量 + */ + public PersonSyncCount handlePerson(List oaPersons, OaOrgIndex orgIndex) { + if (CollectionUtil.isEmpty(oaPersons)) { + return new PersonSyncCount(0, 0); + } + Map uniquePersonMap = new LinkedHashMap<>(); + int skippedCount = 0; + for (OAPersonResponse oaPerson : oaPersons) { + String account = userConvert.resolveAccount(oaPerson); + if (StringUtils.isBlank(account)) { + skippedCount++; + continue; + } + uniquePersonMap.putIfAbsent(account, oaPerson); + } + if (uniquePersonMap.isEmpty()) { + ComposeLogUtil.getLastLog().warn("OA人员均缺少loginid/工号/手机号,跳过人员同步"); + return new PersonSyncCount(0, skippedCount); + } + String tenantId = resolveTenantId(); + String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD)); + List users = uniquePersonMap.values().stream() + .map(person -> { + User user = userConvert.person2user(person, defaultPassword); + user.setTenantId(tenantId); + return user; + }) + .toList(); + List accounts = users.stream() + .map(User::getAccount) + .filter(StringUtils::isNotBlank) + .distinct() + .toList(); + List phones = users.stream() + .map(User::getPhone) + .filter(StringUtils::isNotBlank) + .distinct() + .toList(); + Map existingByAccount = new HashMap<>(); + Map existingByPhone = new HashMap<>(); + userService.list(Wrappers.lambdaQuery() + .and(wrapper -> { + wrapper.in(User::getAccount, accounts); + if (CollectionUtil.isNotEmpty(phones)) { + wrapper.or().in(User::getPhone, phones); + } + }) + ).stream() + .peek(userService::decryptPhone) + .forEach(user -> { + if (StringUtils.isNotBlank(user.getAccount())) { + existingByAccount.put(user.getAccount(), user); + } + if (StringUtils.isNotBlank(user.getPhone())) { + existingByPhone.put(user.getPhone(), user); + } + }); + Map userMap = new HashMap<>(); + Set existsUserIds = new HashSet<>(); + users.forEach(user -> { + User existingUser = existingByAccount.get(user.getAccount()); + if (existingUser == null && StringUtils.isNotBlank(user.getPhone())) { + existingUser = existingByPhone.get(user.getPhone()); + } + if (existingUser != null) { + user.setId(existingUser.getId()); + user.setPassword(null); + existsUserIds.add(existingUser.getId()); + } else { + user.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); + user.setPostId("-1"); + user.setPersonCategory(1); + user.setDataScopeRange(1); + user.setDataLevelRange(1); + user.setIncludeNewCustomer(0); + userService.encryptPhone(user); + } + userMap.put(user.getAccount(), user.getId()); + }); + + List addUsers = users.stream() + .filter(user -> !existsUserIds.contains(user.getId())) + .toList(); + if (CollectionUtil.isNotEmpty(addUsers)) { + ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size()); + userService.saveBatch(addUsers); + } + List updateUsers = users.stream() + .filter(user -> existsUserIds.contains(user.getId())) + .toList(); + if (CollectionUtil.isNotEmpty(updateUsers)) { + ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size()); + userService.updateBatchById(updateUsers); + } + ComposeLogUtil.getLastLog().info("缺少账号已跳过的人员:{}", skippedCount); + if (userMap.isEmpty()) { + return new PersonSyncCount(0, skippedCount); + } + + Map userDeptMap = userDeptService.list(Wrappers.lambdaQuery() + .in(UserDept::getUserId, userMap.values()) + ).stream() + .collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second)); + Set existsUserDeptIds = new HashSet<>(userDeptMap.values()); + List userDeptList = oaPersons.stream() + .filter(oaPerson -> userMap.containsKey(userConvert.resolveAccount(oaPerson))) + .map(oaPerson -> this.buildUserDept(oaPerson, userMap, orgIndex)) + .filter(Objects::nonNull) + .toList(); + userDeptList.forEach(userDept -> { + String userDeptKey = getUserDeptKey(userDept); + if (userDeptMap.containsKey(userDeptKey)) { + userDept.setId(userDeptMap.get(userDeptKey)); + } else { + userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); + } + }); + List addList = userDeptList.stream() + .filter(userDept -> !existsUserDeptIds.contains(userDept.getId())) + .toList(); + if (CollectionUtil.isNotEmpty(addList)) { + ComposeLogUtil.getLastLog().info("批量新增用户部门:{}", addList.size()); + userDeptService.saveBatch(addList); + } + List updateList = userDeptList.stream() + .filter(userDept -> existsUserDeptIds.contains(userDept.getId())) + .toList(); + if (CollectionUtil.isNotEmpty(updateList)) { + ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size()); + userDeptService.updateBatchById(updateList); + } + Collection userIds = userMap.values(); + List list = userDeptService.queryUserDeptIds(userIds); + Set noRoleUserIds = userService.list(Wrappers.lambdaQuery() + .in(User::getId, userIds) + .and(wrapper -> wrapper.isNull(User::getRoleId) + .or().eq(User::getRoleId, "") + .or().eq(User::getRoleId, "-1")) + ).stream() + .map(User::getId) + .collect(Collectors.toSet()); + String defaultRoleId = getDefaultRoleId(); + List updateUserParams = list.stream() + .filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH) + .map(userDeptIds -> { + User user = new User(); + user.setId(userDeptIds.getUserId()); + user.setDeptId(userDeptIds.getDeptIds()); + user.setDeptCodes(userDeptIds.getDeptCodes()); + if (noRoleUserIds.contains(userDeptIds.getUserId())) { + user.setRoleId(defaultRoleId); + } + return user; + }).toList(); + if (CollectionUtil.isNotEmpty(updateUserParams)) { + userService.updateBatchById(updateUserParams); + } + return new PersonSyncCount(users.size(), skippedCount); + } + + private Dept getOrCreateRootCompany() { + Dept rootCompany = deptService.getOne(Wrappers.lambdaQuery() + .eq(Dept::getDeptName, OAConvertConstant.ROOT_COMPANY_NAME) + .last("limit 1"), false); + if (rootCompany != null) { + return rootCompany; + } + rootCompany = new Dept(); + rootCompany.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); + rootCompany.setTenantId(resolveTenantId()); + rootCompany.setParentId(BladeConstant.TOP_PARENT_ID); + rootCompany.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID)); + rootCompany.setDeptName(OAConvertConstant.ROOT_COMPANY_NAME); + rootCompany.setFullName(OAConvertConstant.ROOT_COMPANY_NAME); + rootCompany.setShortName(OAConvertConstant.ROOT_COMPANY_NAME); + rootCompany.setDeptCode("OACROOT"); + rootCompany.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID)); + rootCompany.setBelongCompanyCode("OACROOT"); + rootCompany.setDeptCategory(DeptCategory.COMPANY.getCode()); + rootCompany.setSort(0); + rootCompany.setStatus(DataStatusEnum.ENABLE.getCode()); + rootCompany.setIsDeleted(BladeConstant.DB_NOT_DELETED); + rootCompany.setIsOa(YES); + rootCompany.setIsPlatformCompany(0); + rootCompany.setSyncTime(new Date()); + deptService.save(rootCompany); + ComposeLogUtil.getLastLog().info("已创建顶级组织:{}", OAConvertConstant.ROOT_COMPANY_NAME); + return rootCompany; + } + + private Dept upsertOrg(String name, String oaCode, Dept existing, Dept parent, String tenantId, + DeptCategory deptCategory, List addList, List updateList) { + if (existing != null) { + Dept updateParam = new Dept(); + updateParam.setId(existing.getId()); + updateParam.setDeptName(name); + updateParam.setFullName(name); + updateParam.setShortName(name); + updateParam.setParentId(parent.getId()); + updateParam.setParentCode(parent.getDeptCode()); + updateParam.setAncestors(buildAncestors(parent)); + updateParam.setIsOa(YES); + updateParam.setSyncTime(new Date()); + updateList.add(updateParam); + existing.setDeptName(name); + existing.setFullName(name); + existing.setShortName(name); + existing.setParentId(parent.getId()); + existing.setParentCode(parent.getDeptCode()); + existing.setAncestors(updateParam.getAncestors()); + return existing; + } + Dept dept = this.buildOrgDept(name, oaCode, parent, tenantId, deptCategory); + dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null)); + addList.add(dept); + return dept; + } + + private void saveOrgs(List addList, List updateList, DeptCategory deptCategory) { + if (CollectionUtil.isNotEmpty(addList)) { + ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size()); + deptService.saveBatch(addList); + } + if (CollectionUtil.isNotEmpty(updateList)) { + ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size()); + deptService.updateBatchById(updateList); + } + } + + private Dept buildOrgDept(String name, String deptCode, Dept parent, String tenantId, DeptCategory deptCategory) { + Dept dept = new Dept(); + dept.setTenantId(tenantId); + dept.setParentId(parent.getId()); + dept.setParentCode(parent.getDeptCode()); + dept.setAncestors(this.buildAncestors(parent)); + dept.setDeptName(name); + dept.setFullName(name); + dept.setShortName(name); + dept.setDeptCode(deptCode); + dept.setBelongCompanyCode(DeptCategory.COMPANY.equals(deptCategory) ? deptCode : parent.getBelongCompanyCode()); + dept.setDeptCategory(deptCategory.getCode()); + dept.setSort(0); + dept.setStatus(DataStatusEnum.ENABLE.getCode()); + dept.setIsDeleted(BladeConstant.DB_NOT_DELETED); + dept.setIsOa(YES); + dept.setIsPlatformCompany(0); + dept.setSyncTime(new Date()); + return dept; + } + + private UserDept buildUserDept(OAPersonResponse oaPerson, Map userMap, OaOrgIndex orgIndex) { + Dept department = orgIndex.findDept(oaPerson); + if (department == null || department.getId() == null) { + return null; + } + UserDept userDept = userConvert.person2userDept(oaPerson, userMap); + if (userDept.getUserId() == null) { + return null; + } + Dept company = orgIndex.findCompany(oaPerson); + userDept.setDeptId(department.getId()); + userDept.setDeptCode(department.getDeptCode()); + userDept.setDeptName(department.getDeptName()); + if (company != null) { + userDept.setCompanyCode(company.getDeptCode()); + userDept.setCompanyName(company.getDeptName()); + } + return userDept; + } + + private String getDefaultRoleId() { + String defaultRole = ParamCache.getValue(DEFAULT_PARAM_ROLE); + if (defaultRole == null) { + defaultRole = DEFAULT_ROLE; + } + List roleList = roleService.list(Wrappers.lambdaQuery() + .eq(Role::getRoleAlias, defaultRole) + ); + if (CollectionUtil.isEmpty(roleList)) { + return null; + } + return roleList.get(0).getId().toString(); + } + + private String resolveCompanyKey(OAPersonResponse oaPerson) { + if (oaPerson == null) { + return null; + } + if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) { + return oaPerson.getSubcompanyid1().trim(); + } + if (StringUtils.isNotBlank(oaPerson.getSubcompanyname())) { + return "NAME:" + oaPerson.getSubcompanyname().trim(); + } + return null; + } + + private String resolveDepartmentKey(OAPersonResponse oaPerson) { + if (oaPerson == null) { + return null; + } + if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) { + return oaPerson.getDepartmentid().trim(); + } + String companyKey = resolveCompanyKey(oaPerson); + if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())) { + return companyKey + ":" + oaPerson.getDepartmentname().trim(); + } + return null; + } + + private String buildCompanyCode(OAPersonResponse oaPerson, String companyKey) { + if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) { + return OAConvertConstant.COMPANY_OA_PREFIX + oaPerson.getSubcompanyid1().trim(); + } + return OAConvertConstant.COMPANY_OA_PREFIX + "N" + Math.abs(companyKey.hashCode()); + } + + private String buildDepartmentCode(OAPersonResponse oaPerson, String departmentKey) { + if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) { + return OAConvertConstant.DEPARTMENT_OA_PREFIX + oaPerson.getDepartmentid().trim(); + } + return OAConvertConstant.DEPARTMENT_OA_PREFIX + "N" + Math.abs(departmentKey.hashCode()); + } + + private String buildAncestors(Dept parent) { + String ancestors = parent.getAncestors(); + if (StringUtils.isBlank(ancestors)) { + ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID); + } + return ancestors + "," + parent.getId(); + } + + private String resolveTenantId() { + String tenantId = AuthUtil.getTenantId(); + if (StringUtils.isBlank(tenantId)) { + return BladeConstant.ADMIN_TENANT_ID; + } + return tenantId; + } + + private String getUserDeptKey(UserDept userDept) { + if (userDept == null) { + return null; + } + return userDept.getUserId() + userDept.getCompanyCode() + userDept.getDeptCode(); + } + + /** + * 本批人员同步计数 + */ + public static class PersonSyncCount { + private final int syncedCount; + private final int skippedCount; + + public PersonSyncCount(int syncedCount, int skippedCount) { + this.syncedCount = syncedCount; + this.skippedCount = skippedCount; + } + + public int getSyncedCount() { + return syncedCount; + } + + public int getSkippedCount() { + return skippedCount; + } + } + + /** + * OA 组织索引 + */ + public static class OaOrgIndex { + private final Map companyByOaId = new HashMap<>(); + private final Map deptByOaId = new HashMap<>(); + private final Map deptByCompanyAndName = new HashMap<>(); + + private Dept findCompany(OAPersonResponse oaPerson) { + if (oaPerson == null) { + return null; + } + if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) { + Dept company = companyByOaId.get(oaPerson.getSubcompanyid1().trim()); + if (company != null) { + return company; + } + } + if (StringUtils.isNotBlank(oaPerson.getSubcompanyname())) { + return companyByOaId.get("NAME:" + oaPerson.getSubcompanyname().trim()); + } + return null; + } + + private Dept findDept(OAPersonResponse oaPerson) { + if (oaPerson == null) { + return null; + } + if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) { + Dept department = deptByOaId.get(oaPerson.getDepartmentid().trim()); + if (department != null) { + return department; + } + } + String companyKey = StringUtils.isNotBlank(oaPerson.getSubcompanyid1()) + ? oaPerson.getSubcompanyid1().trim() + : (StringUtils.isNotBlank(oaPerson.getSubcompanyname()) ? "NAME:" + oaPerson.getSubcompanyname().trim() : null); + if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())) { + Dept department = deptByCompanyAndName.get(companyKey + "#" + oaPerson.getDepartmentname()); + if (department != null) { + return department; + } + return deptByOaId.get(companyKey + ":" + oaPerson.getDepartmentname().trim()); + } + return null; + } + } +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java new file mode 100644 index 0000000..f6f26d9 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserPhoneServiceImpl.java @@ -0,0 +1,212 @@ +/** + * 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.system.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.cache.utils.CacheUtil; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.resource.feign.ISmsClient; +import org.springblade.resource.utils.SmsUtil; +import org.springblade.system.pojo.dto.PhoneChangeDTO; +import org.springblade.system.pojo.dto.PhoneVerifyDTO; +import org.springblade.system.pojo.entity.User; +import org.springblade.system.service.IUserPhoneService; +import org.springblade.system.service.IUserService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.regex.Pattern; + +import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE; + +/** + * 用户手机号变更服务实现 + * + * @author Chill + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserPhoneServiceImpl implements IUserPhoneService { + + /** + * 与登录短信一致,对应后台 /resource/sms 的 smsCode + */ + private static final String SMS_RESOURCE_CODE = "ali_reg"; + + /** + * 原手机号已校验凭证(Redis) + */ + private static final String PHONE_CHANGE_VERIFIED_KEY = "blade:user:phone:change:verified:"; + + private static final Duration PHONE_CHANGE_VERIFIED_TTL = Duration.ofMinutes(15); + + private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); + + private final IUserService userService; + private final ISmsClient smsClient; + private final BladeRedis bladeRedis; + + @Override + public R sendCode(String phone) { + String normalizedPhone = normalizePhone(phone); + Long userId = AuthUtil.getUserId(); + if (Func.isEmpty(userId)) { + throw new ServiceException("请先登录"); + } + User currentUser = requireCurrentUser(userId); + String tenantId = Func.toStr(currentUser.getTenantId(), AuthUtil.getTenantId()); + boolean isCurrentPhone = StringUtil.equals(normalizedPhone, Func.toStr(currentUser.getPhone())); + if (!isCurrentPhone) { + assertPhoneAvailable(tenantId, normalizedPhone, userId); + } + R result = smsClient.sendValidate(tenantId, SMS_RESOURCE_CODE, normalizedPhone); + if (result == null || !result.isSuccess()) { + return R.fail(SmsUtil.SEND_FAIL); + } + return R.data(result.getData(), SmsUtil.SEND_SUCCESS); + } + + @Override + public boolean verifyOldPhone(PhoneVerifyDTO phoneVerify) { + Long userId = AuthUtil.getUserId(); + if (Func.isEmpty(userId)) { + throw new ServiceException("请先登录"); + } + String id = Func.toStr(phoneVerify.getId()).trim(); + String code = Func.toStr(phoneVerify.getCode()).trim(); + if (StringUtil.isBlank(id) || StringUtil.isBlank(code)) { + throw new ServiceException("请先获取并填写验证码"); + } + User currentUser = requireCurrentUser(userId); + String oldPhone = Func.toStr(currentUser.getPhone()).trim(); + if (StringUtil.isBlank(oldPhone)) { + throw new ServiceException("当前账号未绑定手机号"); + } + validateSms(currentUser.getTenantId(), id, code, oldPhone); + bladeRedis.setEx(PHONE_CHANGE_VERIFIED_KEY + userId, "1", PHONE_CHANGE_VERIFIED_TTL); + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean changePhone(PhoneChangeDTO phoneChange) { + Long userId = AuthUtil.getUserId(); + if (Func.isEmpty(userId)) { + throw new ServiceException("请先登录"); + } + String verified = Func.toStr(bladeRedis.get(PHONE_CHANGE_VERIFIED_KEY + userId)); + if (!StringUtil.equals(verified, "1")) { + throw new ServiceException("请先完成原手机号验证"); + } + String id = Func.toStr(phoneChange.getId()).trim(); + String code = Func.toStr(phoneChange.getCode()).trim(); + String newPhone = normalizePhone(phoneChange.getNewPhone()); + if (StringUtil.isBlank(id) || StringUtil.isBlank(code)) { + throw new ServiceException("请先获取并填写验证码"); + } + User currentUser = requireCurrentUser(userId); + String oldPhone = Func.toStr(currentUser.getPhone()).trim(); + if (StringUtil.equals(newPhone, oldPhone)) { + throw new ServiceException("新手机号不可与当前手机号相同"); + } + String tenantId = Func.toStr(currentUser.getTenantId(), AuthUtil.getTenantId()); + assertPhoneAvailable(tenantId, newPhone, userId); + validateSms(tenantId, id, code, newPhone); + + User updateUser = new User(); + updateUser.setId(userId); + updateUser.setPhone(newPhone); + // 账号若等于原手机号,同步更新,保证短信登录可用 + if (StringUtil.isNotBlank(oldPhone) && StringUtil.equals(oldPhone, Func.toStr(currentUser.getAccount()))) { + assertAccountAvailable(tenantId, newPhone, userId); + updateUser.setAccount(newPhone); + } + boolean updated = userService.updateById(updateUser); + if (!updated) { + throw new ServiceException("手机号修改失败"); + } + bladeRedis.del(PHONE_CHANGE_VERIFIED_KEY + userId); + CacheUtil.clear(USER_CACHE); + return true; + } + + private User requireCurrentUser(Long userId) { + User user = userService.getById(userId); + if (user == null) { + throw new ServiceException("用户不存在"); + } + return user; + } + + private void validateSms(String tenantId, String id, String value, String phone) { + R result = smsClient.validateMessage(tenantId, SMS_RESOURCE_CODE, id, value, phone); + if (result == null || !result.isSuccess()) { + throw new ServiceException(SmsUtil.VALIDATE_FAIL); + } + } + + private void assertPhoneAvailable(String tenantId, String phone, Long excludeUserId) { + Long phoneCount = userService.count( + Wrappers.lambdaQuery() + .eq(User::getTenantId, tenantId) + .eq(User::getPhone, phone) + .ne(User::getId, excludeUserId) + ); + if (phoneCount != null && phoneCount > 0L) { + throw new ServiceException(StringUtil.format("当前手机 [{}] 已存在!", phone)); + } + } + + private void assertAccountAvailable(String tenantId, String account, Long excludeUserId) { + Long accountCount = userService.count( + Wrappers.lambdaQuery() + .eq(User::getTenantId, tenantId) + .eq(User::getAccount, account) + .ne(User::getId, excludeUserId) + ); + if (accountCount != null && accountCount > 0L) { + throw new ServiceException(StringUtil.format("当前用户 [{}] 已存在!", account)); + } + } + + private String normalizePhone(String phone) { + String normalizedPhone = Func.toStr(phone).trim(); + if (!MOBILE_PATTERN.matcher(normalizedPhone).matches()) { + throw new ServiceException("手机号格式不正确"); + } + return normalizedPhone; + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index 3eb7d43..575adb7 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -529,6 +529,46 @@ public class UserServiceImpl extends BaseServiceImpl implement return userInfo; } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) { + if (Func.isBlank(tenantId) || Func.isEmpty(userId) || Func.isBlank(openid)) { + throw new ServiceException("绑定微信 openid 参数不完整"); + } + String source = "WECHAT_MINI"; + UserOauth byOpenId = userOauthService.getOne(Wrappers.lambdaQuery() + .eq(UserOauth::getTenantId, tenantId) + .eq(UserOauth::getSource, source) + .eq(UserOauth::getUuid, openid) + .last("LIMIT 1")); + if (byOpenId != null) { + byOpenId.setUserId(userId); + if (Func.isNotBlank(phone)) { + byOpenId.setUsername(phone); + } + return userOauthService.updateById(byOpenId); + } + UserOauth byUser = userOauthService.getOne(Wrappers.lambdaQuery() + .eq(UserOauth::getTenantId, tenantId) + .eq(UserOauth::getSource, source) + .eq(UserOauth::getUserId, userId) + .last("LIMIT 1")); + if (byUser != null) { + byUser.setUuid(openid); + if (Func.isNotBlank(phone)) { + byUser.setUsername(phone); + } + return userOauthService.updateById(byUser); + } + UserOauth oauth = new UserOauth(); + oauth.setTenantId(tenantId); + oauth.setUserId(userId); + oauth.setUuid(openid); + oauth.setUsername(Func.toStr(phone, "")); + oauth.setSource(source); + return userOauthService.save(oauth); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean grant(String userIds, String roleIds) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java index 3a64704..2fc52aa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java @@ -30,7 +30,6 @@ 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.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest; import org.springblade.transport.pojo.entity.ExceptionDisposal; @@ -48,7 +47,7 @@ import org.springframework.web.bind.annotation.RestController; *

* 对外路径:{@code /api/blade-transport/exception-disposal/**} * 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。 - * 司机上报(submit)/ 列表 / 详情仅需登录态;跟进与完成保留菜单鉴权。 + * 列表 / 详情 / 上报 / 跟进 / 完成均仅需登录态(小程序调度端与司机端共用)。 */ @RestController @AllArgsConstructor @@ -80,27 +79,24 @@ public class ExceptionDisposalController extends BladeController { } @PostMapping("/follow") - @PreAuth(menu = "exception_disposal") @ApiOperationSupport(order = 4) - @Operation(summary = "异常跟进") + @Operation(summary = "异常跟进", description = "调度端跟进;仅需登录态") public R follow(@RequestBody ExceptionDisposalFollowRequest request) { exceptionDisposalService.follow(request); return R.success("跟进成功"); } @PostMapping("/complete") - @PreAuth(menu = "exception_disposal") @ApiOperationSupport(order = 5) - @Operation(summary = "完成异常") + @Operation(summary = "完成异常", description = "调度端结案;仅需登录态") public R complete(@RequestBody ExceptionDisposalFollowRequest request) { exceptionDisposalService.complete(request.getId()); return R.success("完成成功"); } @PostMapping("/batch-complete") - @PreAuth(menu = "exception_disposal") @ApiOperationSupport(order = 6) - @Operation(summary = "批量完成异常") + @Operation(summary = "批量完成异常", description = "调度端批量结案;仅需登录态") public R batchComplete(@RequestParam String ids) { exceptionDisposalService.batchComplete(ids); return R.success("批量完成成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java new file mode 100644 index 0000000..119cf5b --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java @@ -0,0 +1,143 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.AdminDriverOptionVO; +import org.springblade.transport.pojo.vo.AdminHomeStatsVO; +import org.springblade.transport.pojo.vo.AdminHomeVO; +import org.springblade.transport.pojo.vo.AdminVehicleOptionVO; +import org.springblade.transport.pojo.vo.AdminWaybillCardVO; +import org.springblade.transport.pojo.vo.AdminWaybillDetailVO; +import org.springblade.transport.service.IManageWaybillService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 调度端运单(小程序管理端) + *

+ * 对外完整路径:{@code /api/blade-transport/waybill/manage/**} + * (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /waybill/manage/**})。 + * 同时兼容未去前缀直连({@code /blade-transport/waybill/manage/**})。 + * 仅需登录态,不挂管理端菜单鉴权。 + */ +@RestController +@AllArgsConstructor +@RequestMapping({"/waybill/manage", "/blade-transport/waybill/manage"}) +@Tag(name = "调度端运单", description = "小程序调度端首页统计与运单列表") +public class ManageWaybillController extends BladeController { + + private final IManageWaybillService manageWaybillService; + + @GetMapping("/stats") + @ApiOperationSupport(order = 1) + @Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;租户内不过滤组织(小程序调度账号组织常与运单不一致);在途异常=异常处置状态≠已完成") + public R stats() { + return R.data(manageWaybillService.stats()); + } + + @GetMapping("/home") + @ApiOperationSupport(order = 2) + @Operation(summary = "首页聚合", description = "统计 + 异常/风险角标 + 待处理事项(异常处置≠已完成)+ 当前用户名") + public R home() { + return R.data(manageWaybillService.home()); + } + + @GetMapping("/list") + @ApiOperationSupport(order = 3) + @Operation(summary = "运单分页列表", description = "当前组织运单;status:0待接单/1运输中/2已完成;exception:exception/normal;transportType:common/load") + public R> list( + @Parameter(description = "当前页") @RequestParam(required = false) Integer current, + @Parameter(description = "每页条数") @RequestParam(required = false) Integer size, + @Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword, + @Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status, + @Parameter(description = "异常:exception有异常/normal无异常") @RequestParam(required = false) String exception, + @Parameter(description = "运输组织:common普通/load配载") @RequestParam(required = false) String transportType, + @Parameter(description = "创建日起 YYYY-MM-DD") @RequestParam(required = false) String startDate, + @Parameter(description = "创建日止 YYYY-MM-DD") @RequestParam(required = false) String endDate) { + return R.data(manageWaybillService.pageList( + current, size, keyword, status, exception, transportType, startDate, endDate)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 4) + @Operation(summary = "运单详情", description = "调度端查看运单详情(含 punchNodes / enrouteRecords),不校验司机归属与组织;字段对齐小程序 pages/waybill/detail") + public R detail( + @Parameter(description = "运单ID", required = true) @RequestParam Long id) { + return R.data(manageWaybillService.detail(id)); + } + + @GetMapping("/pending") + @ApiOperationSupport(order = 5) + @Operation(summary = "待处理运单", description = "待接单/运输中;needReassign=true 仅司机已拒单") + public R> pending( + @Parameter(description = "当前页") @RequestParam(required = false) Integer current, + @Parameter(description = "每页条数") @RequestParam(required = false) Integer size, + @Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword, + @Parameter(description = "是否需重新派单") @RequestParam(required = false) Boolean needReassign) { + return R.data(manageWaybillService.pendingList(current, size, keyword, needReassign)); + } + + @PostMapping("/reassign") + @ApiOperationSupport(order = 6) + @Operation(summary = "重新派单", description = "小程序调度端:跳过组织校验,仅需登录态;传入运单ID及新司机、手机号、车牌") + public R reassign(@RequestBody Waybill waybill) { + return R.status(manageWaybillService.reassign( + waybill.getId(), + waybill.getDriverId(), + waybill.getDriverName(), + waybill.getDriverPhone(), + waybill.getVehicleNo())); + } + + @GetMapping("/driver-search") + @ApiOperationSupport(order = 7) + @Operation(summary = "搜索司机", description = "按姓名/手机号模糊搜索,供重新派单选用") + public R> driverSearch( + @Parameter(description = "关键字") @RequestParam(required = false) String keyword) { + return R.data(manageWaybillService.searchDrivers(keyword)); + } + + @GetMapping("/vehicle-search") + @ApiOperationSupport(order = 8) + @Operation(summary = "搜索车牌", description = "按车牌模糊搜索(来自司机绑定车牌)") + public R> vehicleSearch( + @Parameter(description = "关键字") @RequestParam(required = false) String keyword) { + return R.data(manageWaybillService.searchVehicles(keyword)); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java index f9e804d..be43425 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java @@ -68,6 +68,12 @@ public interface IDriverWaybillService { */ DriverWaybillCardVO detail(Long id); + /** + * 按运单ID组装详情打卡数据(punchNodes / enrouteRecords),不校验当前登录人是否为该司机。 + * 供调度端 manage/detail 复用。 + */ + DriverWaybillCardVO detailPunchSnapshot(Long id); + /** * 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IManageWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IManageWaybillService.java new file mode 100644 index 0000000..08bd411 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IManageWaybillService.java @@ -0,0 +1,90 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.transport.pojo.vo.AdminDriverOptionVO; +import org.springblade.transport.pojo.vo.AdminHomeStatsVO; +import org.springblade.transport.pojo.vo.AdminHomeVO; +import org.springblade.transport.pojo.vo.AdminVehicleOptionVO; +import org.springblade.transport.pojo.vo.AdminWaybillCardVO; +import org.springblade.transport.pojo.vo.AdminWaybillDetailVO; + +import java.util.List; + +/** + * 调度端(小程序管理端)运单首页服务 + */ +public interface IManageWaybillService { + + /** + * 运单状态统计:运输中 / 待接单 / 在途异常 / 已完成 + */ + AdminHomeStatsVO stats(); + + /** + * 首页聚合:统计 + 角标 + 待处理事项(异常处置≠已完成)+ 用户名 + */ + AdminHomeVO home(); + + /** + * 调度端运单分页列表 + * + * @param current 页码 + * @param size 每页条数 + * @param keyword 运单号/司机/车牌 + * @param status 0待接单/1运输中/2已完成,空=全部 + * @param exception exception有异常 / normal无异常 / 空=全部 + * @param transportType common普通 / load配载 / 空=全部 + * @param startDate 创建日起 YYYY-MM-DD + * @param endDate 创建日止 YYYY-MM-DD + */ + IPage pageList(Integer current, Integer size, String keyword, String status, + String exception, String transportType, String startDate, String endDate); + + /** + * 调度端运单详情(不校验司机归属) + */ + AdminWaybillDetailVO detail(Long id); + + /** + * 待处理运单(待接单 / 运输中;可筛需重新派单) + */ + IPage pendingList(Integer current, Integer size, String keyword, Boolean needReassign); + + /** + * 重新派单:跳过管理端部门校验,仅需登录态(司机、手机号、车牌) + */ + boolean reassign(Long id, Long driverId, String driverName, String driverPhone, String vehicleNo); + + /** + * 搜索司机(姓名/手机号) + */ + List searchDrivers(String keyword); + + /** + * 搜索车牌(来自司机绑定车牌) + */ + List searchVehicles(String keyword); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java index e4f77e6..bf3dccf 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java @@ -60,6 +60,12 @@ public interface IWaybillService extends BaseService { boolean maintainMileage(WaybillMileageRequest request); boolean cancel(Long id); boolean reassign(Waybill waybill); + + /** + * 小程序调度端重新派单:跳过管理端部门校验,其余逻辑与 {@link #reassign(Waybill)} 一致。 + */ + boolean reassignWithoutDeptCheck(Waybill waybill); + boolean complete(Long id); /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index ad1cbc1..2181abc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -225,6 +225,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { return toCard(normalizeAcceptStatus(waybill), true); } + @Override + public DriverWaybillCardVO detailPunchSnapshot(Long id) { + if (id == null) { + throw new ServiceException("运单ID不能为空"); + } + Waybill waybill = waybillService.getById(id); + if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) { + throw new ServiceException("运单不存在"); + } + return toCard(normalizeAcceptStatus(waybill), true); + } + @Override @Transactional(rollbackFor = Exception.class) public DriverEnrouteRecordVO submitEnroute(EnrouteSubmitDTO dto) { @@ -610,6 +622,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setAcceptStatus(waybill.getDriverAcceptStatus()); card.setRejectReason(waybill.getDriverRejectReason()); + // 详情页字段(列表也可带上,体积很小) + String cargoName = Func.toStr(waybill.getCargoName(), ""); + String weightText = card.getWeight(); + card.setCargoName(cargoName); + card.setPickupAddress(card.getFromAddress()); + card.setUnloadAddress(card.getToAddress()); + card.setCargoQuantity(weightText); + card.setTotalWeight(weightText); + card.setTransportType(toTransportTypeLabel(waybill.getTransportType())); + card.setPlanShipTime(formatLocalDateYmd(waybill.getEstimatedStartTime())); + card.setPlanFinishTime(formatLocalDateYmd(waybill.getEstimatedEndTime())); + card.setRemark(Func.toStr(waybill.getRemark(), "")); + if (withEnrouteRecords) { Date lastPunchAt = findLastPunchTime(waybill.getId()); WaybillProcessSupport.TransitCheckinDecision transit = WaybillProcessSupport.evaluateTransitCheckin( @@ -623,6 +648,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setTransitTimeEnd(transit.timeEnd()); card.setEnrouteRecords(listEnrouteRecords(waybill.getId())); card.setPunchNodes(buildPunchNodes(waybill, transit, processJson)); + card.setRoutePoints(buildSimpleRoutePoints(waybill)); + card.setProcessJson(processJson); } else { // 列表/首页:只解析过程配置是否启用在途打卡,不做频次/时段与落库查询 boolean punchEnabled = WaybillProcessSupport.isTransitPunchEnabled(processJson); @@ -634,18 +661,61 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { return card; } + private List buildSimpleRoutePoints(Waybill waybill) { + DriverWaybillCardVO.DriverRoutePointVO load = new DriverWaybillCardVO.DriverRoutePointVO(); + load.setName(Func.toStr(waybill.getDepartureName(), "装货点")); + load.setAddress(Func.toStr(waybill.getDepartureAddress(), load.getName())); + load.setStatus("pending"); + DriverWaybillCardVO.DriverRoutePointVO unload = new DriverWaybillCardVO.DriverRoutePointVO(); + unload.setName(Func.toStr(waybill.getArrivalName(), "卸货点")); + unload.setAddress(Func.toStr(waybill.getArrivalAddress(), unload.getName())); + unload.setStatus("pending"); + return List.of(load, unload); + } + + private String toTransportTypeLabel(String transportType) { + if (Func.isBlank(transportType)) { + return ""; + } + String t = transportType.trim().toLowerCase(); + return switch (t) { + case "road", "gl" -> "公路运输"; + case "railway", "rail" -> "铁路运输"; + case "river", "water", "waterway" -> "水路运输"; + case "air", "aviation" -> "航空运输"; + default -> transportType; + }; + } + + private String formatLocalDateYmd(LocalDate date) { + if (date == null) { + return ""; + } + return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + /** - * 动态获取项目启用中的过程配置节点 JSON;无则回退运单快照 processJson。 + * 优先用项目启用中的过程配置;若动态配置无打卡节点,回退运单快照 processJson, + * 避免项目配置改坏后司机端打卡页空白。 */ private String resolveProcessJson(Waybill waybill) { if (waybill == null) { return null; } + String snapshot = waybill.getProcessJson(); String live = loadLiveProcessConfigJson(waybill.getProjectId()); if (Func.isNotEmpty(live)) { + if (!WaybillProcessSupport.listDriverPunchNodes(live).isEmpty()) { + return live; + } + // 动态配置存在但无可打卡节点:仍回退快照 + if (Func.isNotEmpty(snapshot) + && !WaybillProcessSupport.listDriverPunchNodes(snapshot).isEmpty()) { + return snapshot; + } return live; } - return waybill.getProcessJson(); + return snapshot; } private String loadLiveProcessConfigJson(Long projectId) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java new file mode 100644 index 0000000..5854689 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java @@ -0,0 +1,642 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.Driver; +import org.springblade.transport.pojo.entity.ExceptionDisposal; +import org.springblade.transport.pojo.entity.RiskDisposal; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.AdminDriverOptionVO; +import org.springblade.transport.pojo.vo.AdminHomeBadgesVO; +import org.springblade.transport.pojo.vo.AdminHomeStatsVO; +import org.springblade.transport.pojo.vo.AdminHomeVO; +import org.springblade.transport.pojo.vo.AdminTodoItemVO; +import org.springblade.transport.pojo.vo.AdminVehicleOptionVO; +import org.springblade.transport.pojo.vo.AdminWaybillCardVO; +import org.springblade.transport.pojo.vo.AdminWaybillDetailVO; +import org.springblade.transport.pojo.vo.DriverWaybillCardVO; +import org.springblade.transport.service.IDriverService; +import org.springblade.transport.service.IDriverWaybillService; +import org.springblade.transport.service.IExceptionDisposalService; +import org.springblade.transport.service.IManageWaybillService; +import org.springblade.transport.service.IRiskDisposalService; +import org.springblade.transport.service.IWaybillService; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 调度端:运单状态统计 + 异常/风险角标 + 待处理事项 + 运单列表 + *

+ * 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 dept 过滤, + * 仅依赖租户隔离,口径接近后台 {@code /waybill-manage/list?allDept=1}。 + */ +@Service +@RequiredArgsConstructor +public class ManageWaybillServiceImpl implements IManageWaybillService { + + private static final String STATUS_PENDING = "pending"; + private static final String STATUS_RUNNING = "running"; + private static final String STATUS_COMPLETED = "completed"; + private static final String STATUS_CANCELLED = "cancelled"; + private static final String ACCEPT_REJECTED = "rejected"; + + private static final String DISPOSAL_PENDING = "pending"; + private static final String DISPOSAL_PROCESSING = "processing"; + private static final String RISK_PENDING = "pending"; + + private static final String EXCEPTION_YES = "exception"; + private static final String EXCEPTION_NO = "normal"; + private static final String TRANSPORT_COMMON = "common"; + private static final String TRANSPORT_LOAD = "load"; + + private static final int FEED_LIMIT = 20; + private static final int DEFAULT_PAGE_SIZE = 10; + private static final int MAX_PAGE_SIZE = 50; + + private static final DateTimeFormatter DATE_MD = DateTimeFormatter.ofPattern("MM-dd"); + private static final DateTimeFormatter DATE_YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final IWaybillService waybillService; + private final IDriverService driverService; + private final IExceptionDisposalService exceptionDisposalService; + private final IRiskDisposalService riskDisposalService; + private final IDriverWaybillService driverWaybillService; + + @Override + public AdminHomeStatsVO stats() { + AdminHomeStatsVO vo = new AdminHomeStatsVO(); + vo.setPendingAccept(countWaybillByStatus(STATUS_PENDING)); + vo.setTransporting(countWaybillByStatus(STATUS_RUNNING)); + vo.setCompleted(countWaybillByStatus(STATUS_COMPLETED)); + vo.setException(countIncompleteExceptions()); + return vo; + } + + @Override + public AdminHomeVO home() { + AdminHomeVO home = new AdminHomeVO(); + home.setUserName(resolveUserName()); + home.setStats(stats()); + + AdminHomeBadgesVO badges = new AdminHomeBadgesVO(); + badges.setException(home.getStats().getException()); + badges.setRisk(countPendingRisks()); + home.setBadges(badges); + home.setFeed(buildExceptionFeed()); + return home; + } + + @Override + public IPage pageList(Integer current, Integer size, String keyword, String status, + String exception, String transportType, String startDate, String endDate) { + int pageNo = current == null || current < 1 ? 1 : current; + int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE); + + Set exceptionWaybillIds = loadIncompleteExceptionWaybillIds(); + if (EXCEPTION_YES.equals(exception) && exceptionWaybillIds.isEmpty()) { + Page emptyVo = new Page<>(pageNo, pageSize, 0); + emptyVo.setRecords(List.of()); + return emptyVo; + } + + LambdaQueryWrapper wrapper = scopedWaybillQuery(); + applyStatusFilter(wrapper, status); + applyExceptionFilter(wrapper, exception, exceptionWaybillIds); + applyTransportTypeFilter(wrapper, transportType); + applyKeywordFilter(wrapper, keyword); + applyCreateTimeFilter(wrapper, startDate, endDate); + wrapper.orderByDesc(Waybill::getCreateTime); + + IPage entityPage = waybillService.page(new Page<>(pageNo, pageSize), wrapper); + List pageIds = entityPage.getRecords().stream() + .map(Waybill::getId) + .filter(Objects::nonNull) + .toList(); + Set pageExceptionIds = pageIds.isEmpty() + ? Collections.emptySet() + : exceptionWaybillIds.stream().filter(pageIds::contains).collect(Collectors.toSet()); + + Page voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal()); + voPage.setRecords(entityPage.getRecords().stream() + .map(w -> toCard(w, pageExceptionIds.contains(w.getId()))) + .toList()); + return voPage; + } + + @Override + public AdminWaybillDetailVO detail(Long id) { + if (id == null) { + throw new org.springblade.core.log.exception.ServiceException("运单ID不能为空"); + } + Waybill waybill = waybillService.getById(id); + if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) { + throw new org.springblade.core.log.exception.ServiceException("运单不存在"); + } + boolean hasException = false; + Long exceptionId = null; + ExceptionDisposal latest = exceptionDisposalService.getOne(Wrappers.lambdaQuery() + .eq(ExceptionDisposal::getWaybillId, id) + .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) + .orderByDesc(ExceptionDisposal::getReportTime) + .orderByDesc(ExceptionDisposal::getCreateTime) + .last("LIMIT 1")); + if (latest != null) { + hasException = true; + exceptionId = latest.getId(); + } + return toDetail(waybill, hasException, exceptionId); + } + + @Override + public IPage pendingList(Integer current, Integer size, String keyword, Boolean needReassign) { + int pageNo = current == null || current < 1 ? 1 : current; + int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE); + + LambdaQueryWrapper wrapper = scopedWaybillQuery() + .in(Waybill::getBusinessStatus, STATUS_PENDING, STATUS_RUNNING); + if (Boolean.TRUE.equals(needReassign)) { + wrapper.eq(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED); + } else if (Boolean.FALSE.equals(needReassign)) { + wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus) + .or().ne(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED)); + } + applyKeywordFilter(wrapper, keyword); + wrapper.orderByDesc(Waybill::getUpdateTime).orderByDesc(Waybill::getCreateTime); + + IPage entityPage = waybillService.page(new Page<>(pageNo, pageSize), wrapper); + Set exceptionWaybillIds = loadIncompleteExceptionWaybillIds(); + List pageIds = entityPage.getRecords().stream() + .map(Waybill::getId) + .filter(Objects::nonNull) + .toList(); + Set pageExceptionIds = pageIds.isEmpty() + ? Collections.emptySet() + : exceptionWaybillIds.stream().filter(pageIds::contains).collect(Collectors.toSet()); + + Page voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal()); + voPage.setRecords(entityPage.getRecords().stream() + .map(w -> toCard(w, pageExceptionIds.contains(w.getId()))) + .toList()); + return voPage; + } + + private AdminWaybillDetailVO toDetail(Waybill waybill, boolean hasException, Long exceptionId) { + AdminWaybillDetailVO detail = new AdminWaybillDetailVO(); + detail.setId(waybill.getId()); + detail.setWaybillNo(waybill.getWaybillNo()); + detail.setStatus(toAppStatus(waybill.getBusinessStatus())); + + String mode = Func.toStr(waybill.getTransportType(), ""); + detail.setTransportMode(mode); + detail.setTransportType(toTransportTypeLabel(mode)); + detail.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON); + + detail.setFromName(formatPlaceName(waybill.getDepartureName(), mode)); + detail.setToName(formatPlaceName(waybill.getArrivalName(), mode)); + String fromAddr = Func.toStr(waybill.getDepartureAddress(), Func.toStr(waybill.getDepartureName(), "")); + String toAddr = Func.toStr(waybill.getArrivalAddress(), Func.toStr(waybill.getArrivalName(), "")); + detail.setFromAddress(fromAddr); + detail.setToAddress(toAddr); + detail.setPickupAddress(fromAddr); + detail.setUnloadAddress(toAddr); + + String cargo = Func.toStr(waybill.getCargoName(), ""); + String weight = formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()); + detail.setCargoName(cargo); + detail.setCargoQuantity(weight); + detail.setWeight(weight); + detail.setTotalWeight(weight); + + detail.setPlanShipTime(formatLocalDate(waybill.getEstimatedStartTime())); + detail.setPlanFinishTime(formatLocalDate(waybill.getEstimatedEndTime())); + detail.setCarrierName(Func.toStr(waybill.getCarrierName(), "")); + detail.setDriverName(Func.toStr(waybill.getDriverName(), "")); + detail.setDriverPhone(Func.toStr(waybill.getDriverPhone(), "")); + detail.setVehicleNo(Func.toStr(waybill.getVehicleNo(), "")); + detail.setRemark(Func.toStr(waybill.getRemark(), "")); + detail.setHasException(hasException); + detail.setExceptionId(exceptionId); + detail.setNeedReassign(ACCEPT_REJECTED.equals(waybill.getDriverAcceptStatus())); + detail.setAcceptStatus(Func.toStr(waybill.getDriverAcceptStatus(), "")); + detail.setRejectReason(Func.toStr(waybill.getDriverRejectReason(), "")); + detail.setDriverId(waybill.getDriverId()); + + AdminWaybillDetailVO.AdminRoutePointVO load = new AdminWaybillDetailVO.AdminRoutePointVO(); + load.setName(Func.toStr(waybill.getDepartureName(), "装货点")); + load.setAddress(fromAddr); + load.setStatus("pending"); + AdminWaybillDetailVO.AdminRoutePointVO unload = new AdminWaybillDetailVO.AdminRoutePointVO(); + unload.setName(Func.toStr(waybill.getArrivalName(), "卸货点")); + unload.setAddress(toAddr); + unload.setStatus("pending"); + detail.setRoutePoints(List.of(load, unload)); + + // 复用司机端打卡组装:过程节点 + 途打卡记录(调度端只读展示) + DriverWaybillCardVO punch = driverWaybillService.detailPunchSnapshot(waybill.getId()); + if (punch != null) { + detail.setPunchNodes(punch.getPunchNodes()); + detail.setEnrouteRecords(punch.getEnrouteRecords()); + if (punch.getRoutePoints() != null && !punch.getRoutePoints().isEmpty()) { + detail.setRoutePoints(punch.getRoutePoints().stream().map(p -> { + AdminWaybillDetailVO.AdminRoutePointVO rp = new AdminWaybillDetailVO.AdminRoutePointVO(); + rp.setName(p.getName()); + rp.setAddress(p.getAddress()); + rp.setStatus(p.getStatus()); + return rp; + }).toList()); + } + } + return detail; + } + + @Override + public boolean reassign(Long id, Long driverId, String driverName, String driverPhone, String vehicleNo) { + Waybill request = new Waybill(); + request.setId(id); + request.setDriverId(driverId); + request.setDriverName(driverName); + request.setDriverPhone(driverPhone); + request.setVehicleNo(vehicleNo); + return waybillService.reassignWithoutDeptCheck(request); + } + + @Override + public List searchDrivers(String keyword) { + String key = Func.toStr(keyword, "").trim(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(Driver::getIsDeleted, 0) + .orderByDesc(Driver::getUpdateTime) + .last("LIMIT 20"); + if (Func.isNotBlank(key)) { + wrapper.and(w -> w.like(Driver::getDriverName, key).or().like(Driver::getMobile, key)); + } + return driverService.list(wrapper).stream().map(d -> { + AdminDriverOptionVO vo = new AdminDriverOptionVO(); + vo.setId(d.getId()); + vo.setName(Func.toStr(d.getDriverName(), "")); + vo.setPhone(Func.toStr(d.getMobile(), "")); + vo.setVehicleNo(Func.toStr(d.getDrivingVehicle(), "")); + return vo; + }).toList(); + } + + @Override + public List searchVehicles(String keyword) { + String key = Func.toStr(keyword, "").trim(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(Driver::getIsDeleted, 0) + .isNotNull(Driver::getDrivingVehicle) + .ne(Driver::getDrivingVehicle, "") + .orderByDesc(Driver::getUpdateTime) + .last("LIMIT 30"); + if (Func.isNotBlank(key)) { + wrapper.like(Driver::getDrivingVehicle, key); + } + java.util.LinkedHashMap map = new java.util.LinkedHashMap<>(); + for (Driver d : driverService.list(wrapper)) { + String plate = Func.toStr(d.getDrivingVehicle(), "").trim(); + if (Func.isBlank(plate) || map.containsKey(plate)) { + continue; + } + AdminVehicleOptionVO vo = new AdminVehicleOptionVO(); + vo.setVehicleNo(plate); + vo.setDriverName(Func.toStr(d.getDriverName(), "")); + map.put(plate, vo); + } + return new java.util.ArrayList<>(map.values()); + } + + /** 运输方式字典值 → 展示文案 */ + private String toTransportTypeLabel(String transportType) { + if (Func.isBlank(transportType)) { + return ""; + } + String t = transportType.trim().toLowerCase(); + return switch (t) { + case "road", "gl" -> "公路运输"; + case "railway", "rail" -> "铁路运输"; + case "river", "water", "waterway" -> "水路运输"; + case "air", "aviation" -> "航空运输"; + default -> transportType; + }; + } + + private long countWaybillByStatus(String status) { + return waybillService.count(Wrappers.lambdaQuery() + .eq(Waybill::getBusinessStatus, status)); + } + + /** + * 小程序调度端不做组织过滤。 + * 「小程序管理」等账号 JWT/档案 dept 常与运单业务组织不一致,按 dept 过滤会导致统计全 0; + * 与后台 allDept=1 一致,仅依赖租户隔离(MyBatis-Plus TenantLine)。 + */ + private LambdaQueryWrapper scopedWaybillQuery() { + return Wrappers.lambdaQuery(); + } + + private void applyStatusFilter(LambdaQueryWrapper wrapper, String status) { + String businessStatus = toBusinessStatus(status); + if (Func.isNotBlank(businessStatus)) { + wrapper.eq(Waybill::getBusinessStatus, businessStatus); + } + } + + private void applyExceptionFilter(LambdaQueryWrapper wrapper, String exception, Set exceptionWaybillIds) { + if (EXCEPTION_YES.equals(exception)) { + wrapper.in(Waybill::getId, exceptionWaybillIds); + } else if (EXCEPTION_NO.equals(exception) && !exceptionWaybillIds.isEmpty()) { + wrapper.notIn(Waybill::getId, exceptionWaybillIds); + } + } + + private void applyTransportTypeFilter(LambdaQueryWrapper wrapper, String transportType) { + if (TRANSPORT_LOAD.equals(transportType)) { + wrapper.isNotNull(Waybill::getLoadingNo).ne(Waybill::getLoadingNo, ""); + } else if (TRANSPORT_COMMON.equals(transportType)) { + wrapper.and(w -> w.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, "")); + } + } + + private void applyKeywordFilter(LambdaQueryWrapper wrapper, String keyword) { + if (Func.isBlank(keyword)) { + return; + } + String key = keyword.trim(); + wrapper.and(w -> w.like(Waybill::getWaybillNo, key) + .or().like(Waybill::getDriverName, key) + .or().like(Waybill::getVehicleNo, key)); + } + + private void applyCreateTimeFilter(LambdaQueryWrapper wrapper, String startDate, String endDate) { + if (Func.isNotBlank(startDate)) { + Date start = DateUtil.parse(startDate.trim() + " 00:00:00", DateUtil.PATTERN_DATETIME); + if (start != null) { + wrapper.ge(Waybill::getCreateTime, start); + } + } + if (Func.isNotBlank(endDate)) { + Date end = DateUtil.parse(endDate.trim() + " 23:59:59", DateUtil.PATTERN_DATETIME); + if (end != null) { + wrapper.le(Waybill::getCreateTime, end); + } + } + } + + /** 小程序 status → 后端 businessStatus */ + private String toBusinessStatus(String status) { + if (Func.isBlank(status)) { + return null; + } + return switch (status.trim()) { + case "0", STATUS_PENDING -> STATUS_PENDING; + case "1", STATUS_RUNNING, "transporting", "doing" -> STATUS_RUNNING; + case "2", STATUS_COMPLETED, "done" -> STATUS_COMPLETED; + case "3", STATUS_CANCELLED -> STATUS_CANCELLED; + default -> null; + }; + } + + private Set loadIncompleteExceptionWaybillIds() { + List list = exceptionDisposalService.list(Wrappers.lambdaQuery() + .select(ExceptionDisposal::getWaybillId) + .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) + .isNotNull(ExceptionDisposal::getWaybillId)); + Set ids = new HashSet<>(); + for (ExceptionDisposal item : list) { + if (item.getWaybillId() != null) { + ids.add(item.getWaybillId()); + } + } + return ids; + } + + private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) { + AdminWaybillCardVO card = new AdminWaybillCardVO(); + card.setId(waybill.getId()); + card.setWaybillNo(waybill.getWaybillNo()); + String mode = Func.toStr(waybill.getTransportType(), ""); + card.setTransportMode(mode); + card.setFromName(formatPlaceName(waybill.getDepartureName(), mode)); + card.setToName(formatPlaceName(waybill.getArrivalName(), mode)); + card.setCargo(Func.toStr(waybill.getCargoName(), "")); + card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())); + card.setPlanTime(formatLocalDate(waybill.getEstimatedStartTime())); + card.setPlanTimeEnd(formatLocalDate(waybill.getEstimatedEndTime())); + card.setStatus(toAppStatus(waybill.getBusinessStatus())); + card.setCarrierName(Func.toStr(waybill.getCarrierName(), "")); + card.setDriverName(Func.toStr(waybill.getDriverName(), "")); + card.setVehicleNo(Func.toStr(waybill.getVehicleNo(), "")); + card.setHasException(hasException); + card.setTransportType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON); + card.setCreateTime(formatDateTime(waybill.getCreateTime())); + card.setNeedReassign(ACCEPT_REJECTED.equals(waybill.getDriverAcceptStatus())); + card.setBuyerPaid(false); + return card; + } + + /** + * 公路运输:起/终仅展示市县(去掉省/自治区);其它运输方式原样返回。 + */ + private String formatPlaceName(String name, String transportType) { + String raw = Func.toStr(name, "").trim(); + if (Func.isBlank(raw) || !isRoadTransport(transportType)) { + return raw; + } + return toCityCounty(raw); + } + + private boolean isRoadTransport(String transportType) { + if (Func.isBlank(transportType)) { + return false; + } + String t = transportType.trim().toLowerCase(); + return t.contains("road") || transportType.contains("公路") || transportType.contains("道路") || "gl".equals(t); + } + + /** 去掉省级前缀,保留「市 + 区/县/旗」 */ + private String toCityCounty(String name) { + String s = name.replaceFirst("^.+?(省|自治区|特别行政区)", ""); + if (Func.isBlank(s)) { + s = name; + } + java.util.regex.Matcher city = java.util.regex.Pattern + .compile("^(.+?市)(.+?(?:区|县|旗|市))?") + .matcher(s); + if (city.find()) { + return Func.toStr(city.group(1), "") + Func.toStr(city.group(2), ""); + } + java.util.regex.Matcher prefecture = java.util.regex.Pattern + .compile("^(.+?(?:州|盟|地区))(.+?(?:区|县|旗|市))?") + .matcher(s); + if (prefecture.find()) { + return Func.toStr(prefecture.group(1), "") + Func.toStr(prefecture.group(2), ""); + } + return s; + } + + private Integer toAppStatus(String businessStatus) { + if (Func.isBlank(businessStatus)) { + return null; + } + return switch (businessStatus) { + case STATUS_PENDING, "waiting_dispatch", "dispatching" -> 0; + case STATUS_RUNNING -> 1; + case STATUS_COMPLETED -> 2; + case STATUS_CANCELLED -> 3; + default -> null; + }; + } + + private String formatWeight(BigDecimal quantity, String unit) { + if (quantity == null) { + return ""; + } + String qty = quantity.stripTrailingZeros().toPlainString(); + return Func.isBlank(unit) ? qty : qty + unit; + } + + private String formatLocalDate(LocalDate date) { + if (date == null) { + return ""; + } + return date.format(DATE_YMD); + } + + private String formatDateTime(Date date) { + if (date == null) { + return ""; + } + return DateUtil.format(date, DateUtil.PATTERN_DATETIME); + } + + private long countIncompleteExceptions() { + return exceptionDisposalService.count(Wrappers.lambdaQuery() + .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)); + } + + private long countPendingRisks() { + return riskDisposalService.count(Wrappers.lambdaQuery() + .eq(RiskDisposal::getDisposalStatus, RISK_PENDING)); + } + + private List buildExceptionFeed() { + List list = exceptionDisposalService.list(Wrappers.lambdaQuery() + .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) + .orderByDesc(ExceptionDisposal::getReportTime) + .last("LIMIT " + FEED_LIMIT)); + return list.stream().map(this::toTodoItem).collect(Collectors.toList()); + } + + private AdminTodoItemVO toTodoItem(ExceptionDisposal disposal) { + AdminTodoItemVO item = new AdminTodoItemVO(); + item.setId(disposal.getId()); + item.setType("exception"); + item.setTitle("异常待处置"); + item.setTimeAgo(formatTimeAgo(disposal.getReportTime() != null + ? disposal.getReportTime() + : toLocalDateTime(disposal.getCreateTime()))); + item.setDesc(buildExceptionDesc(disposal)); + item.setWaybillNo(Func.toStr(disposal.getWaybillNo(), "")); + item.setActionLabel("立即处置"); + String status = Func.toStr(disposal.getDisposalStatus(), DISPOSAL_PENDING); + item.setTargetUrl("/subpackages/admin/exception?status=" + status); + return item; + } + + private String buildExceptionDesc(ExceptionDisposal disposal) { + String reporter = Func.toStr(disposal.getReporterName(), "司机"); + String type = Func.toStr(disposal.getExceptionType(), "异常"); + String reason = Func.isNotBlank(disposal.getExceptionReason()) + ? disposal.getExceptionReason() + : Func.toStr(disposal.getReportDescription(), ""); + if (Func.isBlank(reason)) { + return reporter + "上报" + type; + } + String text = reporter + "上报" + type + ":" + reason.trim(); + return text.length() > 80 ? text.substring(0, 80) + "…" : text; + } + + private String resolveUserName() { + String realName = UserCache.getUserRealName(AuthUtil.getUserId()); + if (Func.isNotBlank(realName)) { + return realName; + } + return Func.toStr(AuthUtil.getUserName(), ""); + } + + private String formatTimeAgo(LocalDateTime time) { + if (time == null) { + return ""; + } + Duration duration = Duration.between(time, LocalDateTime.now()); + if (duration.isNegative()) { + duration = Duration.ZERO; + } + long minutes = duration.toMinutes(); + if (minutes < 1) { + return "刚刚"; + } + if (minutes < 60) { + return minutes + "分钟"; + } + long hours = duration.toHours(); + if (hours < 24) { + return hours + "小时"; + } + long days = duration.toDays(); + if (days < 30) { + return days + "天"; + } + return time.format(DATE_MD); + } + + private LocalDateTime toLocalDateTime(Date date) { + if (date == null) { + return null; + } + return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime(); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 14e3c3b..b837dd8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -819,10 +819,20 @@ public class WaybillServiceImpl extends BaseServiceImpl @Override @Transactional(rollbackFor = Exception.class) public boolean reassign(Waybill request) { + return doReassign(request, true); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean reassignWithoutDeptCheck(Waybill request) { + return doReassign(request, false); + } + + private boolean doReassign(Waybill request, boolean checkDept) { if (request == null || Func.isEmpty(request.getId())) { throw new ServiceException("运单ID不能为空"); } - Waybill waybill = loadEditable(request.getId(), true); + Waybill waybill = loadEditable(request.getId(), checkDept); assertNotLoaded(waybill); if (!"pending".equals(waybill.getBusinessStatus()) && !"running".equals(waybill.getBusinessStatus())) { throw new ServiceException("仅待执行/进行中运单允许重新派单"); diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java index c0b9cf4..8917a2d 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java @@ -1,9 +1,13 @@ package org.springblade.thirdparty.oa.config; +import feign.Request; import feign.RequestInterceptor; import jakarta.annotation.Resource; +import org.springblade.core.tool.utils.StringUtil; import org.springframework.context.annotation.Bean; +import java.util.concurrent.TimeUnit; + /** * @author bfhuange * @since 2024/12/18 @@ -16,8 +20,23 @@ public class OAFeignClientConfig { public RequestInterceptor requestInterceptor() { return template -> { // 空实现屏蔽 全局拦截器 BladeFeignRequestInterceptor - String authorization=oaProperties.getAuthorization(); - template.header("Authorization",authorization); + template.header("Authorization", normalizeAuthorization(oaProperties.getAuthorization())); }; } + + @Bean + public Request.Options options() { + return new Request.Options(10, TimeUnit.SECONDS, 120, TimeUnit.SECONDS, true); + } + + private String normalizeAuthorization(String authorization) { + if (StringUtil.isBlank(authorization)) { + return authorization; + } + if (StringUtil.startsWithIgnoreCase(authorization, "Basic ") + || StringUtil.startsWithIgnoreCase(authorization, "Bearer ")) { + return authorization; + } + return "Basic " + authorization; + } } diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java index 2182784..4f6027c 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/constant/OAConvertConstant.java @@ -30,4 +30,8 @@ public class OAConvertConstant { * 根公司父id */ public static final Long ROOT_PARENT_ID = 0L; + /** + * 同步人员时挂载的顶级组织名称 + */ + public static final String ROOT_COMPANY_NAME = "桂物物流集团"; } diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java index b571416..9b36edd 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java @@ -42,6 +42,6 @@ public interface IOAClient { * @param param * @return */ - @PostMapping("${thirdParty.oa.queryPersonPageUrl:/api/hrm/resful/getHrmUserInfoWithPage}") + @PostMapping("${thirdParty.oa.queryPersonPageUrl:/gwzh/OA/OA_GET_USER_LIST}") OAResponse queryPersonPage(@RequestBody OASearch param); } diff --git a/blade-third-party-api/blade-wechat-api/pom.xml b/blade-third-party-api/blade-wechat-api/pom.xml new file mode 100644 index 0000000..7926d20 --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + org.springblade + blade-third-party-api + ${revision} + + + blade-wechat-api + ${project.artifactId} + jar + 微信小程序:code2session / 手机号 / openid + + + + org.springblade + blade-core-tool + + + org.springframework.boot + spring-boot-autoconfigure + + + diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/ThirdPartyWechatAutoConfiguration.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/ThirdPartyWechatAutoConfiguration.java new file mode 100644 index 0000000..b708fb2 --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/ThirdPartyWechatAutoConfiguration.java @@ -0,0 +1,14 @@ +package org.springblade.thirdparty.wechat.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * 微信小程序第三方能力自动配置 + */ +@Configuration +@ComponentScan("org.springblade.thirdparty.wechat") +@EnableConfigurationProperties(WechatMiniProperties.class) +public class ThirdPartyWechatAutoConfiguration { +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/WechatMiniProperties.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/WechatMiniProperties.java new file mode 100644 index 0000000..8e38eed --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/config/WechatMiniProperties.java @@ -0,0 +1,28 @@ +package org.springblade.thirdparty.wechat.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 微信小程序配置(Nacos:thirdParty.wechat.mini) + */ +@Data +@ConfigurationProperties(prefix = "third-party.wechat.mini") +public class WechatMiniProperties { + + /** + * 小程序 AppId + */ + private String appId; + + /** + * 小程序 AppSecret + */ + private String appSecret; + + /** + * 微信 API 根地址 + */ + private String apiBase = "https://api.weixin.qq.com"; + +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/constant/WechatMiniConstant.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/constant/WechatMiniConstant.java new file mode 100644 index 0000000..cbc6060 --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/constant/WechatMiniConstant.java @@ -0,0 +1,20 @@ +package org.springblade.thirdparty.wechat.constant; + +/** + * 微信小程序常量 + */ +public interface WechatMiniConstant { + + /** blade_user_oauth.source */ + String SOURCE = "WECHAT_MINI"; + + /** + * OAuth2 grant_type(对齐 BladeX OAuth2GranterConstant.WECHAT_APPLET) + */ + String GRANT_TYPE = "wechat_applet"; + + String JSCODE2SESSION_PATH = "/sns/jscode2session"; + String ACCESS_TOKEN_PATH = "/cgi-bin/token"; + String GET_PHONE_NUMBER_PATH = "/wxa/business/getuserphonenumber"; + +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/exception/WechatMiniException.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/exception/WechatMiniException.java new file mode 100644 index 0000000..a89c13d --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/exception/WechatMiniException.java @@ -0,0 +1,16 @@ +package org.springblade.thirdparty.wechat.exception; + +/** + * 微信小程序调用异常 + */ +public class WechatMiniException extends RuntimeException { + + public WechatMiniException(String message) { + super(message); + } + + public WechatMiniException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatPhoneVO.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatPhoneVO.java new file mode 100644 index 0000000..61434f7 --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatPhoneVO.java @@ -0,0 +1,25 @@ +package org.springblade.thirdparty.wechat.pojo.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 微信手机号结果 + */ +@Data +public class WechatPhoneVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 不带区号的手机号 */ + private String phoneNumber; + + /** 带区号手机号 */ + private String purePhoneNumber; + + private String countryCode; + +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatSessionVO.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatSessionVO.java new file mode 100644 index 0000000..255b7b7 --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/pojo/vo/WechatSessionVO.java @@ -0,0 +1,21 @@ +package org.springblade.thirdparty.wechat.pojo.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * jscode2session 结果 + */ +@Data +public class WechatSessionVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String openid; + private String sessionKey; + private String unionid; + +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/IWechatMiniService.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/IWechatMiniService.java new file mode 100644 index 0000000..100713e --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/IWechatMiniService.java @@ -0,0 +1,21 @@ +package org.springblade.thirdparty.wechat.service; + +import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO; +import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO; + +/** + * 微信小程序能力:openid / 手机号 + */ +public interface IWechatMiniService { + + /** + * wx.login code → openid / session_key + */ + WechatSessionVO code2Session(String loginCode); + + /** + * getPhoneNumber 返回的 code → 手机号 + */ + WechatPhoneVO getPhoneNumber(String phoneCode); + +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/impl/WechatMiniServiceImpl.java b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/impl/WechatMiniServiceImpl.java new file mode 100644 index 0000000..b453501 --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/java/org/springblade/thirdparty/wechat/service/impl/WechatMiniServiceImpl.java @@ -0,0 +1,148 @@ +package org.springblade.thirdparty.wechat.service.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.thirdparty.wechat.config.WechatMiniProperties; +import org.springblade.thirdparty.wechat.constant.WechatMiniConstant; +import org.springblade.thirdparty.wechat.exception.WechatMiniException; +import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO; +import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO; +import org.springblade.thirdparty.wechat.service.IWechatMiniService; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * 微信小程序:code2session / getuserphonenumber + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatMiniServiceImpl implements IWechatMiniService { + + /** access_token 提前 200 秒刷新 */ + private static final long TOKEN_REFRESH_AHEAD_MS = 200_000L; + + private final WechatMiniProperties properties; + + private volatile String cachedAccessToken; + private volatile long accessTokenExpireAt; + + @Override + public WechatSessionVO code2Session(String loginCode) { + assertConfigured(); + if (StringUtil.isBlank(loginCode)) { + throw new WechatMiniException("微信登录 code 不能为空"); + } + String url = properties.getApiBase() + WechatMiniConstant.JSCODE2SESSION_PATH + + "?appid=" + properties.getAppId() + + "&secret=" + properties.getAppSecret() + + "&js_code=" + loginCode + + "&grant_type=authorization_code"; + String body = HttpUtil.get(url, 8000); + JSONObject json = parseJson(body, "code2session"); + assertWxOk(json, "获取 openid 失败"); + String openid = json.getStr("openid"); + if (StringUtil.isBlank(openid)) { + throw new WechatMiniException("微信未返回 openid"); + } + WechatSessionVO vo = new WechatSessionVO(); + vo.setOpenid(openid); + vo.setSessionKey(json.getStr("session_key")); + vo.setUnionid(json.getStr("unionid")); + return vo; + } + + @Override + public WechatPhoneVO getPhoneNumber(String phoneCode) { + assertConfigured(); + if (StringUtil.isBlank(phoneCode)) { + throw new WechatMiniException("微信手机号 code 不能为空"); + } + String accessToken = getAccessToken(); + String url = properties.getApiBase() + WechatMiniConstant.GET_PHONE_NUMBER_PATH + + "?access_token=" + accessToken; + Map payload = new HashMap<>(2); + payload.put("code", phoneCode); + String body = HttpRequest.post(url) + .body(JSONUtil.toJsonStr(payload)) + .timeout(8000) + .execute() + .body(); + JSONObject json = parseJson(body, "getuserphonenumber"); + assertWxOk(json, "获取手机号失败"); + JSONObject phoneInfo = json.getJSONObject("phone_info"); + if (phoneInfo == null) { + throw new WechatMiniException("微信未返回手机号信息"); + } + WechatPhoneVO vo = new WechatPhoneVO(); + vo.setPhoneNumber(phoneInfo.getStr("phoneNumber")); + vo.setPurePhoneNumber(phoneInfo.getStr("purePhoneNumber")); + vo.setCountryCode(phoneInfo.getStr("countryCode")); + if (StringUtil.isBlank(vo.getPurePhoneNumber()) && StringUtil.isBlank(vo.getPhoneNumber())) { + throw new WechatMiniException("微信未返回有效手机号"); + } + return vo; + } + + private String getAccessToken() { + long now = System.currentTimeMillis(); + if (StringUtil.isNotBlank(cachedAccessToken) && now < accessTokenExpireAt) { + return cachedAccessToken; + } + synchronized (this) { + now = System.currentTimeMillis(); + if (StringUtil.isNotBlank(cachedAccessToken) && now < accessTokenExpireAt) { + return cachedAccessToken; + } + String url = properties.getApiBase() + WechatMiniConstant.ACCESS_TOKEN_PATH + + "?grant_type=client_credential" + + "&appid=" + properties.getAppId() + + "&secret=" + properties.getAppSecret(); + String body = HttpUtil.get(url, 8000); + JSONObject json = parseJson(body, "getAccessToken"); + assertWxOk(json, "获取 access_token 失败"); + String token = json.getStr("access_token"); + Integer expiresIn = json.getInt("expires_in", 7200); + if (StringUtil.isBlank(token)) { + throw new WechatMiniException("微信未返回 access_token"); + } + cachedAccessToken = token; + accessTokenExpireAt = System.currentTimeMillis() + Math.max(60, expiresIn) * 1000L - TOKEN_REFRESH_AHEAD_MS; + return token; + } + } + + private void assertConfigured() { + if (StringUtil.isBlank(properties.getAppId()) || StringUtil.isBlank(properties.getAppSecret())) { + throw new WechatMiniException("未配置微信小程序 appId/appSecret(Nacos: thirdParty.wechat.mini)"); + } + } + + private JSONObject parseJson(String body, String action) { + if (StringUtil.isBlank(body)) { + throw new WechatMiniException("微信接口无响应: " + action); + } + try { + return JSONUtil.parseObj(body); + } catch (Exception e) { + log.error("解析微信响应失败 action={} body={}", action, body, e); + throw new WechatMiniException("解析微信响应失败: " + action, e); + } + } + + private void assertWxOk(JSONObject json, String fallbackMsg) { + Integer errcode = json.getInt("errcode"); + if (errcode != null && errcode != 0) { + String errmsg = json.getStr("errmsg", fallbackMsg); + throw new WechatMiniException(fallbackMsg + ":" + errmsg + "(" + errcode + ")"); + } + } + +} diff --git a/blade-third-party-api/blade-wechat-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/blade-third-party-api/blade-wechat-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..e386596 --- /dev/null +++ b/blade-third-party-api/blade-wechat-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springblade.thirdparty.wechat.config.ThirdPartyWechatAutoConfiguration diff --git a/blade-third-party-api/pom.xml b/blade-third-party-api/pom.xml index 31f02f5..ba27dd9 100644 --- a/blade-third-party-api/pom.xml +++ b/blade-third-party-api/pom.xml @@ -17,6 +17,7 @@ blade-wps-api blade-ocr-api blade-track-api + blade-wechat-api pom BladeX 第三方API集合 diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index 977a9db..562e977 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -87,6 +87,7 @@ thirdParty: oa: # OA开放接口地址 baseUrl: http://127.0.0.1:8080 + queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST track: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 @@ -120,6 +121,11 @@ thirdParty: downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download} queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list} queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list} + wechat: + mini: + # 微信小程序(手机号一键登录 / openid) + appId: ${WECHAT_MINI_APP_ID:} + appSecret: ${WECHAT_MINI_APP_SECRET:} #百度OCR配置,API Key和Secret Key请通过环境变量注入 baidu: diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 7a749a0..69b2ffa 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -59,6 +59,7 @@ thirdParty: oa: # OA开放接口地址 baseUrl: http://127.0.0.1:8080 + queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST track: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 @@ -92,6 +93,10 @@ thirdParty: downloadFileUrl: ${MK_DOWNLOAD_FILE_URL:/openapi/sys-attach/fileStream/download} queryApprovalListUrl: ${MK_QUERY_APPROVAL_LIST_URL:/openapi/lbpm-approval/lbpmApproval/portal/list} queryProcessListUrl: ${MK_QUERY_PROCESS_LIST_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/list} + wechat: + mini: + appId: ${WECHAT_MINI_APP_ID:} + appSecret: ${WECHAT_MINI_APP_SECRET:} #百度OCR配置,API Key和Secret Key请通过环境变量注入 baidu: diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml index 53fdcb6..f65e82d 100644 --- a/doc/nacos/blade.yaml +++ b/doc/nacos/blade.yaml @@ -213,6 +213,9 @@ blade: #接口放行 skip-url: - /test/** + # 退出登录:允许无令牌/令牌失效时也能调用(服务端对无用户直接返回成功) + - /oauth/logout/** + - /blade-auth/oauth/logout/** #授权认证配置 auth: - method: ALL diff --git a/doc/nacos/routes/blade-gateway-dev.json b/doc/nacos/routes/blade-gateway-dev.json index 09a9740..d68db7a 100644 --- a/doc/nacos/routes/blade-gateway-dev.json +++ b/doc/nacos/routes/blade-gateway-dev.json @@ -27,6 +27,28 @@ "filters": [], "uri": "lb://blade-transport" }, + { + "id": "blade-user-alias-route", + "order": 0, + "predicates": [ + { + "name": "Path", + "args": { + "pattern": "/blade-user/**" + } + } + ], + "filters": [ + { + "name": "RewritePath", + "args": { + "regexp": "/blade-user/(?.*)", + "replacement": "/user/$\\{segment}" + } + } + ], + "uri": "lb://blade-system" + }, { "id": "example-route", "order": 0, diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index 8b6b40d..a3fffa0 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -11,9 +11,15 @@ thirdParty: oa: # OA开放接口地址 baseUrl: ${OA_BASE_URL:http://127.0.0.1:8080} + queryPersonPageUrl: ${OA_QUERY_PERSON_PAGE_URL:/gwzh/OA/OA_GET_USER_LIST} track: # 轨迹开放接口地址 baseUrl: ${TRACK_BASE_URL:http://127.0.0.1:8080} mk: # MK开放接口地址 baseUrl: ${MK_BASE_URL:http://127.0.0.1:8080} + wechat: + mini: + # 微信小程序 AppId / AppSecret(Nacos 配置,勿提交真实 secret) + appId: ${WECHAT_MINI_APP_ID:} + appSecret: ${WECHAT_MINI_APP_SECRET:} diff --git a/doc/sql/update/add-mp-auth-permission.sql b/doc/sql/update/add-mp-auth-permission.sql new file mode 100644 index 0000000..821c983 --- /dev/null +++ b/doc/sql/update/add-mp-auth-permission.sql @@ -0,0 +1,21 @@ +-- 小程序调度端入口权限码 mp_auth +-- user-info 的 permission 来自按钮叶子 code;需把本菜单授权给调度/管理员角色 +-- 执行后:后台「角色管理 → 权限配置」勾选「小程序调度端」,或按需 INSERT blade_role_menu + +-- 父级菜单(小程序) +INSERT IGNORE INTO `blade_menu` +(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) +VALUES +(2091800000000000001, 0, 'miniprogram', '小程序', 'menu', '/miniprogram', 'iconfont iconicon_work', 98, 1, 0, 1, '', '小程序相关权限', 0); + +-- 按钮权限:mp_auth(叶子节点,会被 permissionCodes 收集) +INSERT IGNORE INTO `blade_menu` +(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) +VALUES +(2091800000000000002, 2091800000000000001, 'mp_auth', '小程序调度端', 'mp_auth', '', '', 1, 2, 0, 1, NULL, '登录后进入调度端首页', 0); + +-- 示例:给管理员角色(role_id 按环境实际调整,默认管理员 1123598816738675201) +-- INSERT IGNORE INTO `blade_role_menu` (`id`, `menu_id`, `role_id`) +-- VALUES +-- (2091800000000000101, 2091800000000000001, 1123598816738675201), +-- (2091800000000000102, 2091800000000000002, 1123598816738675201); diff --git a/doc/sql/update/add-wechat-applet-grant-type.sql b/doc/sql/update/add-wechat-applet-grant-type.sql new file mode 100644 index 0000000..8a6a8cd --- /dev/null +++ b/doc/sql/update/add-wechat-applet-grant-type.sql @@ -0,0 +1,8 @@ +-- 为小程序客户端授权类型补充微信小程序登录(wechat_applet) +-- 执行后建议清客户端缓存 / 重启 blade-auth + +UPDATE blade_client +SET authorized_grant_types = CONCAT(authorized_grant_types, ',wechat_applet') +WHERE client_id IN ('saber3', 'saber', 'sword', 'rider') + AND FIND_IN_SET('wechat_applet', REPLACE(authorized_grant_types, ' ', '')) = 0 + AND is_deleted = 0; diff --git a/hs_err_pid22077.log b/hs_err_pid22077.log new file mode 100644 index 0000000..8dc4281 --- /dev/null +++ b/hs_err_pid22077.log @@ -0,0 +1,1379 @@ +# +# A fatal error has been detected by the Java Runtime Environment: +# +# SIGBUS (0xa) at pc=0x0000000100ed64c0, pid=22077, tid=36647 +# +# JRE version: OpenJDK Runtime Environment Zulu17.44+15-CA (17.0.8+7) (build 17.0.8+7-LTS) +# Java VM: OpenJDK 64-Bit Server VM Zulu17.44+15-CA (17.0.8+7-LTS, mixed mode, emulated-client, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64) +# Problematic frame: +# C [libzip.dylib+0x64c0] newEntry+0x68 +# +# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again +# +# If you would like to submit a bug report, please visit: +# http://www.azul.com/support/ +# The crash happened outside the Java Virtual Machine in native code. +# See problematic frame for where to report the bug. +# + +--------------- S U M M A R Y ------------ + +Command Line: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:50259,suspend=y,server=n -javaagent:/Users/liangxin/Library/Caches/JetBrains/IntelliJIdea2026.1/captureAgent/debugger-agent.jar=file:///var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/capture10140205674518191061.props -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Ddebugger.async.stack.trace.for.all.threads=true -Dfile.encoding=UTF-8 org.springblade.desk.DeskApplication + +Host: "Mac15,6" arm64, 12 cores, 36G, Darwin 25.6.0, macOS 26.6.2 (25G83) +Time: Fri Sep 18 14:44:41 2026 CST elapsed time: 9.129557 seconds (0d 0h 0m 9s) + +--------------- T H R E A D --------------- + +Current thread (0x000000012b49c600): JavaThread "sentinel-datafile-log-executor-thread-1" daemon [_thread_in_native, id=36647, stack(0x0000000174680000,0x0000000174883000)] + +Stack: [0x0000000174680000,0x0000000174883000], sp=0x00000001748819d0, free space=2054k +Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) +C [libzip.dylib+0x64c0] newEntry+0x68 +C [libzip.dylib+0x6390] ZIP_GetEntry2+0x14c +C [libzip.dylib+0x6d78] ZIP_FindEntry+0x3c +V [libjvm.dylib+0x25a408] ClassPathZipEntry::open_entry(JavaThread*, char const*, int*, bool)+0xb4 +V [libjvm.dylib+0x25a53c] ClassPathZipEntry::open_stream(JavaThread*, char const*)+0x20 +V [libjvm.dylib+0x25d918] ClassLoader::load_class(Symbol*, bool, JavaThread*)+0x150 +V [libjvm.dylib+0x981d60] SystemDictionary::load_instance_class_impl(Symbol*, Handle, JavaThread*)+0x2d0 +V [libjvm.dylib+0x98063c] SystemDictionary::load_instance_class(unsigned int, Symbol*, Handle, JavaThread*)+0x30 +V [libjvm.dylib+0x97fd48] SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, JavaThread*)+0x4dc +V [libjvm.dylib+0x97f334] SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, JavaThread*)+0x80 +V [libjvm.dylib+0x2beb54] ConstantPool::klass_at_impl(constantPoolHandle const&, int, JavaThread*)+0x1e0 +V [libjvm.dylib+0x46d6f0] InterpreterRuntime::_new(JavaThread*, ConstantPool*, int)+0x94 +j com.intellij.rt.debugger.agent.CaptureStorage$DeepCapturedStack.collectStacks(Ljava/util/List;)Lcom/intellij/rt/debugger/agent/CaptureStorage$StackData;+89 +j com.intellij.rt.debugger.agent.CaptureStorage.getStackTrace(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+30 +j com.intellij.rt.debugger.agent.CaptureStorage.access$1000(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()[Ljava/lang/StackTraceElement;+31 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()Ljava/lang/Object;+1 +j com.intellij.rt.debugger.agent.CaptureStorage.withoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$Callable;)Ljava/lang/Object;+21 +j com.intellij.rt.debugger.agent.CaptureStorage.getAsyncStackTrace(Ljava/lang/Throwable;)[Ljava/lang/StackTraceElement;+19 +j java.lang.Throwable.printStackTrace(Ljava/lang/Throwable$PrintStreamOrWriter;)V+32 java.base@17.0.8 +j java.lang.Throwable.printStackTrace(Ljava/io/PrintWriter;)V+9 java.base@17.0.8 +j com.alibaba.csp.sentinel.log.jul.CspFormatter.format(Ljava/util/logging/LogRecord;)Ljava/lang/String;+103 +j java.util.logging.StreamHandler.publish(Ljava/util/logging/LogRecord;)V+14 java.logging@17.0.8 +j java.util.logging.FileHandler.publish(Ljava/util/logging/LogRecord;)V+11 java.logging@17.0.8 +j com.alibaba.csp.sentinel.log.jul.DateFileLogHandler$LogTask.run()V+8 +j java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+92 java.base@17.0.8 +j java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5 java.base@17.0.8 +j java.lang.Thread.run()V+11 java.base@17.0.8 +v ~StubRoutines::call_stub +V [libjvm.dylib+0x4781f8] JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)+0x394 +V [libjvm.dylib+0x47720c] JavaCalls::call_virtual(JavaValue*, Klass*, Symbol*, Symbol*, JavaCallArguments*, JavaThread*)+0x11c +V [libjvm.dylib+0x4772d8] JavaCalls::call_virtual(JavaValue*, Handle, Klass*, Symbol*, Symbol*, JavaThread*)+0x64 +V [libjvm.dylib+0x52ebfc] thread_entry(JavaThread*, JavaThread*)+0xc4 +V [libjvm.dylib+0x9b22e8] JavaThread::thread_main_inner()+0x150 +V [libjvm.dylib+0x9b0990] Thread::call_run()+0xe0 +V [libjvm.dylib+0x7d0364] thread_native_entry(Thread*)+0x158 +C [libsystem_pthread.dylib+0x6c58] _pthread_start+0x88 + +Java frames: (J=compiled Java code, j=interpreted, Vv=VM code) +j com.intellij.rt.debugger.agent.CaptureStorage$DeepCapturedStack.collectStacks(Ljava/util/List;)Lcom/intellij/rt/debugger/agent/CaptureStorage$StackData;+89 +j com.intellij.rt.debugger.agent.CaptureStorage.getStackTrace(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+30 +j com.intellij.rt.debugger.agent.CaptureStorage.access$1000(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()[Ljava/lang/StackTraceElement;+31 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()Ljava/lang/Object;+1 +j com.intellij.rt.debugger.agent.CaptureStorage.withoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$Callable;)Ljava/lang/Object;+21 +j com.intellij.rt.debugger.agent.CaptureStorage.getAsyncStackTrace(Ljava/lang/Throwable;)[Ljava/lang/StackTraceElement;+19 +j java.lang.Throwable.printStackTrace(Ljava/lang/Throwable$PrintStreamOrWriter;)V+32 java.base@17.0.8 +j java.lang.Throwable.printStackTrace(Ljava/io/PrintWriter;)V+9 java.base@17.0.8 +j com.alibaba.csp.sentinel.log.jul.CspFormatter.format(Ljava/util/logging/LogRecord;)Ljava/lang/String;+103 +j java.util.logging.StreamHandler.publish(Ljava/util/logging/LogRecord;)V+14 java.logging@17.0.8 +j java.util.logging.FileHandler.publish(Ljava/util/logging/LogRecord;)V+11 java.logging@17.0.8 +j com.alibaba.csp.sentinel.log.jul.DateFileLogHandler$LogTask.run()V+8 +j java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+92 java.base@17.0.8 +j java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5 java.base@17.0.8 +j java.lang.Thread.run()V+11 java.base@17.0.8 +v ~StubRoutines::call_stub + +siginfo: si_signo: 10 (SIGBUS), si_code: 1 (BUS_ADRALN), si_addr: 0x0000000100dd1e7b + +Register to memory mapping: + + x0=0x00006000018b9130 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x1=0x0 is NULL + x2=0x0 is NULL + x3=0x00006000018b9140 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x4=0x00006000018b9180 points into unknown readable memory: 0x00000000bf074454 | 54 44 07 bf 00 00 00 00 + x5=0x00000000a0a4a7fb is an unknown value + x6=0x0000000020c00000 is an unknown value + x7=0x000000000000000a is an unknown value + x8=0x0000000100ef9e5f points into unknown readable memory: 00 + x9=0x0000000000128000 is an unknown value +x10=0x00006000018b8000 points into unknown readable memory: 0x4c28004120ed0001 | 01 00 ed 20 41 00 28 4c +x11=0x0000000000001130 is an unknown value +x12=0x0000000000000050 is an unknown value +x13=0x0000000000000001 is an unknown value +x14=0x00000000ffffff6b is an unknown value +x15=0x00000000000007fb is an unknown value +x16=0x0000000186e7d030: __bzero+0 in /usr/lib/system/libsystem_platform.dylib at 0x0000000186e7a000 +x17=0x00000001f4ef54a8 points into unknown readable memory: 0x0000000186e7d030 | 30 d0 e7 86 01 00 00 00 +x18=0x0 is NULL +x19=0x00006000018b9130 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x20=0x0 is NULL +x21=0x0000600000d1c480 points into unknown readable memory: 0x0000600001c14720 | 20 47 c1 01 00 60 00 00 +x22=0x0000000100dd1e5f points into unknown readable memory: 50 +x23=0x00000000d3a18b02 is an unknown value +x24=0x000000000000002f is an unknown value +x25=0x000000000000003d is an unknown value +x26=0x00000000000000cd is an unknown value +x27=0x00006000018b9158 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x28=0x000000010f116600 points into unknown readable memory: 0x65746e692f6d6f63 | 63 6f 6d 2f 69 6e 74 65 + + +Registers: + x0=0x00006000018b9130 x1=0x0000000000000000 x2=0x0000000000000000 x3=0x00006000018b9140 + x4=0x00006000018b9180 x5=0x00000000a0a4a7fb x6=0x0000000020c00000 x7=0x000000000000000a + x8=0x0000000100ef9e5f x9=0x0000000000128000 x10=0x00006000018b8000 x11=0x0000000000001130 +x12=0x0000000000000050 x13=0x0000000000000001 x14=0x00000000ffffff6b x15=0x00000000000007fb +x16=0x0000000186e7d030 x17=0x00000001f4ef54a8 x18=0x0000000000000000 x19=0x00006000018b9130 +x20=0x0000000000000000 x21=0x0000600000d1c480 x22=0x0000000100dd1e5f x23=0x00000000d3a18b02 +x24=0x000000000000002f x25=0x000000000000003d x26=0x00000000000000cd x27=0x00006000018b9158 +x28=0x000000010f116600 fp=0x0000000174881a50 lr=0x0000000100ed648c sp=0x00000001748819d0 +pc=0x0000000100ed64c0 cpsr=0x0000000060001000 +Top of Stack: (sp=0x00000001748819d0) +0x00000001748819d0: 0000000000000000 0000000000000000 +0x00000001748819e0: 0000000000000000 0000000000000000 +0x00000001748819f0: 0000000000000000 0000000000000000 +0x0000000174881a00: 000000010f116600 000000012c81cc00 +0x0000000174881a10: 00000000000000cd 000000000000003d +0x0000000174881a20: 000000000000002f 00000000d3a18b02 +0x0000000174881a30: 00006000018923a0 0000000000000000 +0x0000000174881a40: 000000010f116640 0000600000d1c480 +0x0000000174881a50: 0000000174881ab0 0000000100ed6390 +0x0000000174881a60: 000000010f116600 0000000000000037 +0x0000000174881a70: 0000000000000001 000000010f116640 +0x0000000174881a80: 000000012b49c948 000000010f116640 +0x0000000174881a90: 0000600000d1c480 000000010f116640 +0x0000000174881aa0: 0000000174881bdc 0000000174881af4 +0x0000000174881ab0: 0000000174881ae0 0000000100ed6d78 +0x0000000174881ac0: 0000000174881bdc 0000600003615c50 +0x0000000174881ad0: 0000000000000000 000000012b49c600 +0x0000000174881ae0: 0000000174881bc0 000000010225e408 +0x0000000174881af0: 0000000102abc388 0000000000000100 +0x0000000174881b00: 0000000174881b20 00000001025325dc +0x0000000174881b10: 0000000102abc388 0000000174881ba0 +0x0000000174881b20: 0000000174881b70 00000001022e496c +0x0000000174881b30: 0000000000000000 0000000000000000 +0x0000000174881b40: 0000000000000001 00000001314a0290 +0x0000000174881b50: 000000012b49c600 000000010f1169d8 +0x0000000174881b60: 0000000102ad11e2 0000000174881c68 +0x0000000174881b70: 0000000174881b90 31ade61c7bb700f3 +0x0000000174881b80: 0000000000000001 000000010f116640 +0x0000000174881b90: 0000600003615c50 00000001314a0290 +0x0000000174881ba0: 000000012b49c600 000000010f1169d8 +0x0000000174881bb0: 000000010f1165f0 0000600003615c50 +0x0000000174881bc0: 0000000174881bf0 000000010225e53c + +Instructions: (pc=0x0000000100ed64c0) +0x0000000100ed63c0: 6b0c017f 54ffff60 17ffffde d2800016 +0x0000000100ed63d0: 72001ebf 54000160 b5000156 f100073f +0x0000000100ed63e0: 54fff7cb 8b140328 385ff108 7100bd1f +0x0000000100ed63f0: 54fff741 d2800016 14000002 f9004e7f +0x0000000100ed6400: f9402a60 94000451 aa1603e0 a9457bfd +0x0000000100ed6410: a9444ff4 a94357f6 a9425ff8 a94167fa +0x0000000100ed6420: a8c66ffc d65f03c0 6b03003f 540000e1 +0x0000000100ed6430: 71000421 540000eb 38401408 38401449 +0x0000000100ed6440: 6b09011f 54ffff60 52800000 d65f03c0 +0x0000000100ed6450: 52800020 d65f03c0 d10243ff a9036ffc +0x0000000100ed6460: a90467fa a9055ff8 a90657f6 a9074ff4 +0x0000000100ed6470: a9087bfd 910203fd aa0203f4 aa0103f6 +0x0000000100ed6480: aa0003f5 52800900 94000481 aa0003f3 +0x0000000100ed6490: b4001320 f900027f aa1303fb f8028f7f +0x0000000100ed64a0: f9001a7f 3940c2a8 34000288 f9400ea8 +0x0000000100ed64b0: f94006c9 8b090108 f94016a9 cb090116 +0x0000000100ed64c0: 79403ad8 39407ada 39407edc 794042c8 +0x0000000100ed64d0: f90017e8 b9400ec8 f9000668 b9401ac8 +0x0000000100ed64e0: f9000fe8 f9000a68 794016c8 34000488 +0x0000000100ed64f0: b94016c8 14000023 f94006d7 34000d54 +0x0000000100ed6500: f9401ea8 b4000288 f94022a9 eb17013f +0x0000000100ed6510: 5400022c 5283fa4a 8b0a012a eb17015f +0x0000000100ed6520: 540001ab 9140092a 8b170108 cb090116 +0x0000000100ed6530: 79403ac8 79403ec9 794042cb 8b0802e8 +0x0000000100ed6540: 8b090108 8b0b0108 9100b908 eb0a011f +0x0000000100ed6550: 54000b4d aa1503e0 aa1703e1 52840002 +0x0000000100ed6560: 94000384 aa0003f6 b4000aa0 f9401ea0 +0x0000000100ed6570: 94000429 a903deb6 17ffffd2 d2800008 +0x0000000100ed6580: aa0803f7 f9000e68 b94012c8 b9002268 +0x0000000100ed6590: b842a2c9 f9405ea8 f9000be9 8b090108 +0x0000000100ed65a0: cb0803e8 f9001e68 794012c8 b9004268 +0x0000000100ed65b0: 91000700 94000436 aa0003f9 f9000260 + + +Stack slot to memory mapping: +stack at sp + 0 slots: 0x0 is NULL +stack at sp + 1 slots: 0x0 is NULL +stack at sp + 2 slots: 0x0 is NULL +stack at sp + 3 slots: 0x0 is NULL +stack at sp + 4 slots: 0x0 is NULL +stack at sp + 5 slots: 0x0 is NULL +stack at sp + 6 slots: 0x000000010f116600 points into unknown readable memory: 0x65746e692f6d6f63 | 63 6f 6d 2f 69 6e 74 65 +stack at sp + 7 slots: 0x000000012c81cc00 points into unknown readable memory: 0xffffffff5bbd78a2 | a2 78 bd 5b ff ff ff ff + + +--------------- P R O C E S S --------------- + +Threads class SMR info: +_java_thread_list=0x0000600003f37760, length=73, elements={ +0x000000010b808a00, 0x000000010b809600, 0x000000012c90ac00, 0x000000010b008600, +0x000000010b00ae00, 0x000000011e808200, 0x000000011e00a400, 0x000000010f80a200, +0x000000010b809c00, 0x000000010f80b200, 0x000000011e00aa00, 0x000000010f80b800, +0x000000012d039a00, 0x000000011d808200, 0x000000012c01ea00, 0x000000012d810400, +0x000000012d838200, 0x000000010f829800, 0x000000012b0ffe00, 0x00000001018b8200, +0x000000012dae5600, 0x000000012c39d000, 0x000000011db9d000, 0x000000012c3bba00, +0x000000012c3cb200, 0x000000011dbcc600, 0x000000012db42e00, 0x000000012cedde00, +0x000000012db60800, 0x000000011dc28800, 0x000000012db67000, 0x000000011dc39e00, +0x000000012b336400, 0x000000012c4a3400, 0x000000012cf7fc00, 0x000000010b190600, +0x000000011ead0200, 0x000000011dccd000, 0x000000010b17fa00, 0x000000011e1f1a00, +0x000000010b995e00, 0x000000010f8ffa00, 0x000000011f024a00, 0x000000011dcc2400, +0x000000011eb04e00, 0x000000012d1e3200, 0x000000012d1c0200, 0x000000011e257200, +0x000000010f997000, 0x000000012c55be00, 0x000000012b406400, 0x000000011eb76400, +0x000000010b9b3200, 0x000000010b985800, 0x000000011f02f400, 0x000000012d1f9a00, +0x000000011dd00400, 0x000000011dd2cc00, 0x000000012b437400, 0x000000012b45ba00, +0x000000012b49c600, 0x000000011ebea000, 0x000000011ea07e00, 0x000000011f161800, +0x000000012c5d9e00, 0x000000011f162e00, 0x000000012dd08600, 0x0000000101998600, +0x000000010b1ef200, 0x000000010ba06000, 0x000000011dda3c00, 0x000000011e3d8e00, +0x000000011f24c800 +} + +Java Threads: ( => current thread ) + 0x000000010b808a00 JavaThread "main" [_thread_in_native, id=5891, stack(0x000000016f17c000,0x000000016f37f000)] + 0x000000010b809600 JavaThread "Reference Handler" daemon [_thread_blocked, id=19971, stack(0x000000016ffd0000,0x00000001701d3000)] + 0x000000012c90ac00 JavaThread "Finalizer" daemon [_thread_blocked, id=19715, stack(0x00000001701dc000,0x00000001703df000)] + 0x000000010b008600 JavaThread "Signal Dispatcher" daemon [_thread_blocked, id=30979, stack(0x0000000170500000,0x0000000170703000)] + 0x000000010b00ae00 JavaThread "Service Thread" daemon [_thread_blocked, id=30467, stack(0x000000017070c000,0x000000017090f000)] + 0x000000011e808200 JavaThread "Monitor Deflation Thread" daemon [_thread_blocked, id=23299, stack(0x0000000170918000,0x0000000170b1b000)] + 0x000000011e00a400 JavaThread "C1 CompilerThread0" daemon [_thread_blocked, id=23555, stack(0x0000000170b24000,0x0000000170d27000)] + 0x000000010f80a200 JavaThread "Sweeper thread" daemon [_thread_blocked, id=29699, stack(0x0000000170d30000,0x0000000170f33000)] + 0x000000010b809c00 JavaThread "C1 CompilerThread1" daemon [_thread_blocked, id=24067, stack(0x0000000170f3c000,0x000000017113f000)] + 0x000000010f80b200 JavaThread "Common-Cleaner" daemon [_thread_blocked, id=24323, stack(0x0000000171148000,0x000000017134b000)] + 0x000000011e00aa00 JavaThread "C1 CompilerThread2" daemon [_thread_blocked, id=24835, stack(0x0000000171354000,0x0000000171557000)] + 0x000000010f80b800 JavaThread "C1 CompilerThread3" daemon [_thread_blocked, id=29187, stack(0x0000000171560000,0x0000000171763000)] + 0x000000012d039a00 JavaThread "JDWP Transport Listener: dt_socket" daemon [_thread_blocked, id=25603, stack(0x000000017176c000,0x000000017196f000)] + 0x000000011d808200 JavaThread "JDWP Event Helper Thread" daemon [_thread_blocked, id=25859, stack(0x0000000171978000,0x0000000171b7b000)] + 0x000000012c01ea00 JavaThread "JDWP Command Reader" daemon [_thread_in_native, id=26115, stack(0x0000000171b84000,0x0000000171d87000)] + 0x000000012d810400 JavaThread "IntelliJ Suspend Helper" daemon [_thread_blocked, id=26371, stack(0x0000000171d90000,0x0000000171f93000)] + 0x000000012d838200 JavaThread "Notification Thread" daemon [_thread_blocked, id=26883, stack(0x0000000171f9c000,0x000000017219f000)] + 0x000000010f829800 JavaThread "CoarseTimer" daemon [_thread_blocked, id=27139, stack(0x00000001721a8000,0x00000001723ab000)] + 0x000000012b0ffe00 JavaThread "RMI TCP Accept-0" daemon [_thread_in_native, id=41219, stack(0x0000000173620000,0x0000000173823000)] + 0x00000001018b8200 JavaThread "com.alibaba.nacos.client.logging.0" daemon [_thread_blocked, id=40707, stack(0x0000000173a38000,0x0000000173c3b000)] + 0x000000012dae5600 JavaThread "Attach Listener" daemon [_thread_blocked, id=35331, stack(0x0000000173c44000,0x0000000173e47000)] + 0x000000012c39d000 JavaThread "RMI TCP Connection(2)-127.0.0.1" daemon [_thread_in_native, id=39939, stack(0x0000000173e50000,0x0000000174053000)] + 0x000000011db9d000 JavaThread "RMI Scheduler(0)" daemon [_thread_blocked, id=35587, stack(0x000000017405c000,0x000000017425f000)] + 0x000000012c3bba00 JavaThread "nacos.publisher-com.alibaba.nacos.common.notify.SlowEvent" daemon [_thread_blocked, id=43523, stack(0x00000001750bc000,0x00000001752bf000)] + 0x000000012c3cb200 JavaThread "nacos.publisher-com.alibaba.nacos.client.config.impl.ConfigFuzzyWatchNotifyEvent" daemon [_thread_blocked, id=43779, stack(0x00000001752c8000,0x00000001754cb000)] + 0x000000011dbcc600 JavaThread "nacos.publisher-com.alibaba.nacos.client.config.impl.ConfigFuzzyWatchLoadEvent" daemon [_thread_blocked, id=44291, stack(0x00000001754d4000,0x00000001756d7000)] + 0x000000012db42e00 JavaThread "com.alibaba.nacos.client.auth.ram.identify.watcher.0" daemon [_thread_blocked, id=65027, stack(0x00000001756e0000,0x00000001758e3000)] + 0x000000012cedde00 JavaThread "com.alibaba.nacos.client.login-executor.0" daemon [_thread_blocked, id=36883, stack(0x0000000174a98000,0x0000000174c9b000)] + 0x000000012db60800 JavaThread "com.alibaba.nacos.client.listen-executor.0" daemon [_thread_blocked, id=45059, stack(0x00000001758ec000,0x0000000175aef000)] + 0x000000011dc28800 JavaThread "com.alibaba.nacos.client.fuzzy-watcher-executor.0" daemon [_thread_blocked, id=64771, stack(0x0000000175af8000,0x0000000175cfb000)] + 0x000000012db67000 JavaThread "com.alibaba.nacos.client.remote.worker.0" daemon [_thread_blocked, id=64259, stack(0x0000000175d04000,0x0000000175f07000)] + 0x000000011dc39e00 JavaThread "com.alibaba.nacos.client.remote.worker.1" daemon [_thread_blocked, id=45827, stack(0x0000000175f10000,0x0000000176113000)] + 0x000000012b336400 JavaThread "grpc-nio-worker-ELG-1-1" daemon [_thread_in_native, id=64019, stack(0x000000017611c000,0x000000017631f000)] + 0x000000012c4a3400 JavaThread "grpc-default-executor-0" daemon [_thread_blocked, id=63747, stack(0x0000000176328000,0x000000017652b000)] + 0x000000012cf7fc00 JavaThread "nacos-grpc-client-executor-127.0.0.1-0" daemon [_thread_blocked, id=46595, stack(0x0000000176534000,0x0000000176737000)] + 0x000000010b190600 JavaThread "nacos-grpc-client-executor-127.0.0.1-1" daemon [_thread_blocked, id=63235, stack(0x0000000176740000,0x0000000176943000)] + 0x000000011ead0200 JavaThread "grpc-nio-worker-ELG-1-2" daemon [_thread_in_native, id=62995, stack(0x000000017694c000,0x0000000176b4f000)] + 0x000000011dccd000 JavaThread "nacos-grpc-client-executor-127.0.0.1-2" daemon [_thread_blocked, id=47363, stack(0x0000000176b58000,0x0000000176d5b000)] + 0x000000010b17fa00 JavaThread "nacos-grpc-client-executor-127.0.0.1-3" daemon [_thread_blocked, id=62211, stack(0x0000000176d64000,0x0000000176f67000)] + 0x000000011e1f1a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-4" daemon [_thread_blocked, id=47619, stack(0x0000000176f70000,0x0000000177173000)] + 0x000000010b995e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-5" daemon [_thread_blocked, id=61699, stack(0x000000017717c000,0x000000017737f000)] + 0x000000010f8ffa00 JavaThread "nacos-grpc-client-executor-127.0.0.1-6" daemon [_thread_blocked, id=48387, stack(0x0000000177388000,0x000000017758b000)] + 0x000000011f024a00 JavaThread "nacos.publisher-com.alibaba.nacos.common.ability.AbstractAbilityControlManager$AbilityUpdateEvent" daemon [_thread_blocked, id=61187, stack(0x0000000177594000,0x0000000177797000)] + 0x000000011dcc2400 JavaThread "nacos-grpc-client-executor-127.0.0.1-7" daemon [_thread_blocked, id=48899, stack(0x00000001777a0000,0x00000001779a3000)] + 0x000000011eb04e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-8" daemon [_thread_blocked, id=60675, stack(0x00000001779ac000,0x0000000177baf000)] + 0x000000012d1e3200 JavaThread "nacos-grpc-client-executor-127.0.0.1-9" daemon [_thread_blocked, id=49411, stack(0x0000000177bb8000,0x0000000177dbb000)] + 0x000000012d1c0200 JavaThread "nacos-grpc-client-executor-127.0.0.1-10" daemon [_thread_blocked, id=60163, stack(0x0000000177dc4000,0x0000000177fc7000)] + 0x000000011e257200 JavaThread "nacos-grpc-client-executor-127.0.0.1-11" daemon [_thread_blocked, id=59659, stack(0x0000000328004000,0x0000000328207000)] + 0x000000010f997000 JavaThread "nacos-grpc-client-executor-127.0.0.1-12" daemon [_thread_blocked, id=49923, stack(0x0000000328210000,0x0000000328413000)] + 0x000000012c55be00 JavaThread "nacos-grpc-client-executor-127.0.0.1-13" daemon [_thread_blocked, id=59395, stack(0x000000032841c000,0x000000032861f000)] + 0x000000012b406400 JavaThread "nacos-grpc-client-executor-127.0.0.1-14" daemon [_thread_blocked, id=59139, stack(0x0000000328628000,0x000000032882b000)] + 0x000000011eb76400 JavaThread "nacos-grpc-client-executor-127.0.0.1-15" daemon [_thread_blocked, id=50691, stack(0x0000000328834000,0x0000000328a37000)] + 0x000000010b9b3200 JavaThread "nacos-grpc-client-executor-127.0.0.1-16" daemon [_thread_blocked, id=51203, stack(0x0000000328a40000,0x0000000328c43000)] + 0x000000010b985800 JavaThread "nacos-grpc-client-executor-127.0.0.1-17" daemon [_thread_blocked, id=51715, stack(0x0000000328c4c000,0x0000000328e4f000)] + 0x000000011f02f400 JavaThread "nacos-grpc-client-executor-127.0.0.1-18" daemon [_thread_blocked, id=51971, stack(0x0000000328e58000,0x000000032905b000)] + 0x000000012d1f9a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-19" daemon [_thread_blocked, id=58115, stack(0x0000000329064000,0x0000000329267000)] + 0x000000011dd00400 JavaThread "nacos-grpc-client-executor-127.0.0.1-20" daemon [_thread_blocked, id=57859, stack(0x0000000329270000,0x0000000329473000)] + 0x000000011dd2cc00 JavaThread "nacos-grpc-client-executor-127.0.0.1-21" daemon [_thread_blocked, id=57347, stack(0x000000032947c000,0x000000032967f000)] + 0x000000012b437400 JavaThread "nacos-grpc-client-executor-127.0.0.1-22" daemon [_thread_blocked, id=57091, stack(0x0000000329688000,0x000000032988b000)] + 0x000000012b45ba00 JavaThread "RMI TCP Connection(3)-127.0.0.1" daemon [_thread_in_native, id=39179, stack(0x0000000174268000,0x000000017446b000)] +=>0x000000012b49c600 JavaThread "sentinel-datafile-log-executor-thread-1" daemon [_thread_in_native, id=36647, stack(0x0000000174680000,0x0000000174883000)] + 0x000000011ebea000 JavaThread "sentinel-heartbeat-send-task-thread-1" daemon [_thread_blocked, id=56595, stack(0x0000000329894000,0x0000000329a97000)] + 0x000000011ea07e00 JavaThread "sentinel-command-center-executor-thread-1" daemon [_thread_in_native, id=36139, stack(0x0000000174474000,0x0000000174677000)] + 0x000000011f161800 JavaThread "nacos-grpc-client-executor-127.0.0.1-23" daemon [_thread_blocked, id=36367, stack(0x000000017488c000,0x0000000174a8f000)] + 0x000000012c5d9e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-24" daemon [_thread_blocked, id=53003, stack(0x0000000329aa0000,0x0000000329ca3000)] + 0x000000011f162e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-25" daemon [_thread_blocked, id=56323, stack(0x0000000329cac000,0x0000000329eaf000)] + 0x000000012dd08600 JavaThread "nacos-grpc-client-executor-127.0.0.1-26" daemon [_thread_blocked, id=53763, stack(0x0000000329eb8000,0x000000032a0bb000)] + 0x0000000101998600 JavaThread "nacos-grpc-client-executor-127.0.0.1-27" daemon [_thread_blocked, id=55811, stack(0x000000032a0c4000,0x000000032a2c7000)] + 0x000000010b1ef200 JavaThread "nacos-grpc-client-executor-127.0.0.1-28" daemon [_thread_blocked, id=54019, stack(0x000000032a2d0000,0x000000032a4d3000)] + 0x000000010ba06000 JavaThread "nacos-grpc-client-executor-127.0.0.1-29" daemon [_thread_blocked, id=55043, stack(0x000000032a4dc000,0x000000032a6df000)] + 0x000000011dda3c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-30" daemon [_thread_blocked, id=54787, stack(0x000000032a6e8000,0x000000032a8eb000)] + 0x000000011e3d8e00 JavaThread "sentinel-time-tick-thread" daemon [_thread_blocked, id=31875, stack(0x000000032a8f4000,0x000000032aaf7000)] + 0x000000011f24c800 JavaThread "sentinel-heartbeat-send-task-thread-2" daemon [_thread_blocked, id=65843, stack(0x000000032ab00000,0x000000032ad03000)] + +Other Threads: + 0x000000012b8059d0 VMThread "VM Thread" [stack: 0x000000016fdc4000,0x000000016ffc7000] [id=18435] + 0x000000010f109630 WatcherThread [stack: 0x000000017382c000,0x0000000173a2f000] [id=40963] + 0x000000012af08740 GCTaskThread "GC Thread#0" [stack: 0x000000016f388000,0x000000016f58b000] [id=12035] + 0x000000012ae08da0 GCTaskThread "GC Thread#1" [stack: 0x00000001723b4000,0x00000001725b7000] [id=27395] + 0x000000012ae09060 GCTaskThread "GC Thread#2" [stack: 0x00000001725c0000,0x00000001727c3000] [id=32771] + 0x0000000101107cb0 GCTaskThread "GC Thread#3" [stack: 0x00000001727cc000,0x00000001729cf000] [id=33027] + 0x000000010f30cd30 GCTaskThread "GC Thread#4" [stack: 0x00000001729d8000,0x0000000172bdb000] [id=33283] + 0x000000012ae094f0 GCTaskThread "GC Thread#5" [stack: 0x0000000172be4000,0x0000000172de7000] [id=42755] + 0x000000010f30cff0 GCTaskThread "GC Thread#6" [stack: 0x0000000172df0000,0x0000000172ff3000] [id=42243] + 0x000000012b909b70 GCTaskThread "GC Thread#7" [stack: 0x0000000172ffc000,0x00000001731ff000] [id=41987] + 0x000000010f209c10 GCTaskThread "GC Thread#8" [stack: 0x0000000173208000,0x000000017340b000] [id=34051] + 0x000000010f30d870 GCTaskThread "GC Thread#9" [stack: 0x0000000173414000,0x0000000173617000] [id=34307] + 0x000000012af08e00 ConcurrentGCThread "G1 Main Marker" [stack: 0x000000016f594000,0x000000016f797000] [id=13571] + 0x000000012af09690 ConcurrentGCThread "G1 Conc#0" [stack: 0x000000016f7a0000,0x000000016f9a3000] [id=12547] + 0x000000010f41fcd0 ConcurrentGCThread "G1 Conc#1" [stack: 0x0000000174ca4000,0x0000000174ea7000] [id=37891] + 0x000000010f32ed40 ConcurrentGCThread "G1 Conc#2" [stack: 0x0000000174eb0000,0x00000001750b3000] [id=37379] + 0x000000012af0bbd0 ConcurrentGCThread "G1 Refine#0" [stack: 0x000000016f9ac000,0x000000016fbaf000] [id=16643] + 0x000000010f104080 ConcurrentGCThread "G1 Service" [stack: 0x000000016fbb8000,0x000000016fdbb000] [id=21507] + +Threads with active compile tasks: + +VM state: not at safepoint (normal execution) + +VM Mutex/Monitor currently owned by a thread: None + +Heap address: 0x00000005c0000000, size: 9216 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 + +CDS archive(s) mapped at: [0x0000000500000000-0x0000000500c14000-0x0000000500c14000), size 12664832, SharedBaseAddress: 0x0000000500000000, ArchiveRelocationMode: 1. +Compressed class space mapped at: 0x0000000501000000-0x0000000541000000, reserved size: 1073741824 +Narrow klass base: 0x0000000500000000, Narrow klass shift: 0, Narrow klass range: 0x100000000 + +GC Precious Log: + CPUs: 12 total, 12 available + Memory: 36864M + Large Page Support: Disabled + NUMA Support: Disabled + Compressed Oops: Enabled (Zero based) + Heap Region Size: 8M + Heap Min Capacity: 8M + Heap Initial Capacity: 576M + Heap Max Capacity: 9G + Pre-touch: Disabled + Parallel Workers: 10 + Concurrent Workers: 3 + Concurrent Refinement Workers: 10 + Periodic GC: Disabled + +Heap: + garbage-first heap total 262144K, used 92456K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 9 young (73728K), 3 survivors (24576K) + Metaspace used 63382K, committed 63872K, reserved 1114112K + class space used 8462K, committed 8704K, reserved 1048576K + +Heap Regions: E=young(eden), S=young(survivor), O=old, HS=humongous(starts), HC=humongous(continues), CS=collection set, F=free, OA=open archive, CA=closed archive, TAMS=top-at-mark-start (previous, next) +| 0|0x00000005c0000000, 0x00000005c0800000, 0x00000005c0800000|100%| O| |TAMS 0x00000005c0800000, 0x00000005c0000000| Untracked +| 1|0x00000005c0800000, 0x00000005c1000000, 0x00000005c1000000|100%| O| |TAMS 0x00000005c1000000, 0x00000005c0800000| Untracked +| 2|0x00000005c1000000, 0x00000005c1800000, 0x00000005c1800000|100%| O| |TAMS 0x00000005c1800000, 0x00000005c1000000| Untracked +| 3|0x00000005c1800000, 0x00000005c1800000, 0x00000005c2000000| 0%| F| |TAMS 0x00000005c1800000, 0x00000005c1800000| Untracked +| 4|0x00000005c2000000, 0x00000005c207c000, 0x00000005c2800000| 6%| O| |TAMS 0x00000005c207c000, 0x00000005c2000000| Untracked +| 5|0x00000005c2800000, 0x00000005c2800000, 0x00000005c3000000| 0%| F| |TAMS 0x00000005c2800000, 0x00000005c2800000| Untracked +| 6|0x00000005c3000000, 0x00000005c3000000, 0x00000005c3800000| 0%| F| |TAMS 0x00000005c3000000, 0x00000005c3000000| Untracked +| 7|0x00000005c3800000, 0x00000005c3800000, 0x00000005c4000000| 0%| F| |TAMS 0x00000005c3800000, 0x00000005c3800000| Untracked +| 8|0x00000005c4000000, 0x00000005c4000000, 0x00000005c4800000| 0%| F| |TAMS 0x00000005c4000000, 0x00000005c4000000| Untracked +| 9|0x00000005c4800000, 0x00000005c4800000, 0x00000005c5000000| 0%| F| |TAMS 0x00000005c4800000, 0x00000005c4800000| Untracked +| 10|0x00000005c5000000, 0x00000005c5000000, 0x00000005c5800000| 0%| F| |TAMS 0x00000005c5000000, 0x00000005c5000000| Untracked +| 11|0x00000005c5800000, 0x00000005c5800000, 0x00000005c6000000| 0%| F| |TAMS 0x00000005c5800000, 0x00000005c5800000| Untracked +| 12|0x00000005c6000000, 0x00000005c6000000, 0x00000005c6800000| 0%| F| |TAMS 0x00000005c6000000, 0x00000005c6000000| Untracked +| 13|0x00000005c6800000, 0x00000005c6800000, 0x00000005c7000000| 0%| F| |TAMS 0x00000005c6800000, 0x00000005c6800000| Untracked +| 14|0x00000005c7000000, 0x00000005c71d6220, 0x00000005c7800000| 22%| S|CS|TAMS 0x00000005c7000000, 0x00000005c7000000| Complete +| 15|0x00000005c7800000, 0x00000005c8000000, 0x00000005c8000000|100%| S|CS|TAMS 0x00000005c7800000, 0x00000005c7800000| Complete +| 16|0x00000005c8000000, 0x00000005c8800000, 0x00000005c8800000|100%| S|CS|TAMS 0x00000005c8000000, 0x00000005c8000000| Complete +| 17|0x00000005c8800000, 0x00000005c8800000, 0x00000005c9000000| 0%| F| |TAMS 0x00000005c8800000, 0x00000005c8800000| Untracked +| 18|0x00000005c9000000, 0x00000005c9000000, 0x00000005c9800000| 0%| F| |TAMS 0x00000005c9000000, 0x00000005c9000000| Untracked +| 19|0x00000005c9800000, 0x00000005c9800000, 0x00000005ca000000| 0%| F| |TAMS 0x00000005c9800000, 0x00000005c9800000| Untracked +| 20|0x00000005ca000000, 0x00000005ca000000, 0x00000005ca800000| 0%| F| |TAMS 0x00000005ca000000, 0x00000005ca000000| Untracked +| 21|0x00000005ca800000, 0x00000005ca800000, 0x00000005cb000000| 0%| F| |TAMS 0x00000005ca800000, 0x00000005ca800000| Untracked +| 22|0x00000005cb000000, 0x00000005cb000000, 0x00000005cb800000| 0%| F| |TAMS 0x00000005cb000000, 0x00000005cb000000| Untracked +| 23|0x00000005cb800000, 0x00000005cb800000, 0x00000005cc000000| 0%| F| |TAMS 0x00000005cb800000, 0x00000005cb800000| Untracked +| 24|0x00000005cc000000, 0x00000005cc5eec00, 0x00000005cc800000| 74%| E| |TAMS 0x00000005cc000000, 0x00000005cc000000| Complete +| 25|0x00000005cc800000, 0x00000005cd000000, 0x00000005cd000000|100%| E|CS|TAMS 0x00000005cc800000, 0x00000005cc800000| Complete +| 26|0x00000005cd000000, 0x00000005cd800000, 0x00000005cd800000|100%| E|CS|TAMS 0x00000005cd000000, 0x00000005cd000000| Complete +| 27|0x00000005cd800000, 0x00000005ce000000, 0x00000005ce000000|100%| E|CS|TAMS 0x00000005cd800000, 0x00000005cd800000| Complete +| 64|0x00000005e0000000, 0x00000005e0800000, 0x00000005e0800000|100%| E|CS|TAMS 0x00000005e0000000, 0x00000005e0000000| Complete +| 71|0x00000005e3800000, 0x00000005e4000000, 0x00000005e4000000|100%| E|CS|TAMS 0x00000005e3800000, 0x00000005e3800000| Complete +|1150|0x00000007ff000000, 0x00000007ff778000, 0x00000007ff800000| 93%|OA| |TAMS 0x00000007ff778000, 0x00000007ff000000| Untracked +|1151|0x00000007ff800000, 0x00000007ff880000, 0x0000000800000000| 6%|CA| |TAMS 0x00000007ff880000, 0x00000007ff800000| Untracked + +Card table byte_map: [0x0000000119200000,0x000000011a400000] _byte_map_base: 0x0000000116400000 + +Marking Bits (Prev, Next): (CMBitMap*) 0x000000012c82f250, (CMBitMap*) 0x000000012c82f210 + Prev Bits: [0x0000000141000000, 0x000000014a000000) + Next Bits: [0x0000000138000000, 0x0000000141000000) + +Polling page: 0x0000000100da4000 + +Metaspace: + +Usage: + Non-class: 53.63 MB used. + Class: 8.26 MB used. + Both: 61.90 MB used. + +Virtual space: + Non-class space: 64.00 MB reserved, 53.88 MB ( 84%) committed, 1 nodes. + Class space: 1.00 GB reserved, 8.50 MB ( <1%) committed, 1 nodes. + Both: 1.06 GB reserved, 62.38 MB ( 6%) committed. + +Chunk freelists: + Non-Class: 10.08 MB + Class: 7.50 MB + Both: 17.58 MB + +MaxMetaspaceSize: unlimited +CompressedClassSpaceSize: 1.00 GB +Initial GC threshold: 21.00 MB +Current GC threshold: 99.06 MB +CDS: on +MetaspaceReclaimPolicy: balanced + - commit_granule_bytes: 65536. + - commit_granule_words: 8192. + - virtual_space_node_default_size: 8388608. + - enlarge_chunks_in_place: 1. + - new_chunks_are_fully_committed: 0. + - uncommit_free_chunks: 1. + - use_allocation_guard: 0. + - handle_deallocations: 1. + + +Internal statistics: + +num_allocs_failed_limit: 9. +num_arena_births: 684. +num_arena_deaths: 0. +num_vsnodes_births: 2. +num_vsnodes_deaths: 0. +num_space_committed: 998. +num_space_uncommitted: 0. +num_chunks_returned_to_freelist: 9. +num_chunks_taken_from_freelist: 2682. +num_chunk_merges: 6. +num_chunk_splits: 2009. +num_chunks_enlarged: 1609. +num_inconsistent_stats: 0. + +CodeCache: size=49152Kb used=12939Kb max_used=12939Kb free=36212Kb + bounds [0x000000010c100000, 0x000000010cdb0000, 0x000000010f100000] + total_blobs=6555 nmethods=5927 adapters=554 + compilation: enabled + stopped_count=0, restarted_count=0 + full_count=0 + +Compilation events (20 events): +Event: 9.067 Thread 0x000000010b809c00 nmethod 6207 0x000000010cd9aa90 code [0x000000010cd9ac40, 0x000000010cd9ae18] +Event: 9.067 Thread 0x000000010b809c00 6211 1 org.springframework.beans.factory.config.BeanDefinitionVisitor::visitScope (33 bytes) +Event: 9.067 Thread 0x000000010f80b800 nmethod 6210 0x000000010cd9af90 code [0x000000010cd9b140, 0x000000010cd9b358] +Event: 9.067 Thread 0x000000011e00aa00 nmethod 6208 0x000000010cd9b510 code [0x000000010cd9b700, 0x000000010cd9ba38] +Event: 9.067 Thread 0x000000010b809c00 nmethod 6211 0x000000010cd9bc90 code [0x000000010cd9be40, 0x000000010cd9c058] +Event: 9.068 Thread 0x000000011e00a400 nmethod 6206 0x000000010cd9c210 code [0x000000010cd9c580, 0x000000010cd9d558] +Event: 9.068 Thread 0x000000010b809c00 6212 1 org.springframework.beans.AbstractNestablePropertyAccessor::getWrappedInstance (22 bytes) +Event: 9.068 Thread 0x000000010b809c00 nmethod 6212 0x000000010cd9e010 code [0x000000010cd9e1c0, 0x000000010cd9e338] +Event: 9.073 Thread 0x000000011e00a400 6213 1 java.lang.reflect.Constructor::getParameterTypes (11 bytes) +Event: 9.073 Thread 0x000000011e00a400 nmethod 6213 0x000000010cd9e410 code [0x000000010cd9e5c0, 0x000000010cd9e6f8] +Event: 9.126 Thread 0x000000011e00aa00 6216 1 java.util.regex.Pattern::qtype (39 bytes) +Event: 9.126 Thread 0x000000010b809c00 6217 1 java.util.regex.Pattern::sequence (647 bytes) +Event: 9.126 Thread 0x000000010f80b800 6218 1 java.util.regex.Pattern$BranchConn::study (5 bytes) +Event: 9.126 Thread 0x000000010f80b800 nmethod 6218 0x000000010cd9f090 code [0x000000010cd9f200, 0x000000010cd9f2d8] +Event: 9.126 Thread 0x000000011e00aa00 nmethod 6216 0x000000010cd9f390 code [0x000000010cd9f580, 0x000000010cd9f8b8] +Event: 9.127 Thread 0x000000011e00a400 6219 1 jdk.internal.misc.Unsafe::putReferenceOpaque (9 bytes) +Event: 9.127 Thread 0x000000011e00a400 nmethod 6219 0x000000010cd9fa90 code [0x000000010cd9fc00, 0x000000010cd9fd18] +Event: 9.128 Thread 0x000000010b809c00 nmethod 6217 0x000000010cd9fd90 code [0x000000010cda0180, 0x000000010cda1618] +Event: 9.128 Thread 0x000000011e00aa00 6220 1 java.util.IdentityHashMap::put (137 bytes) +Event: 9.129 Thread 0x000000011e00aa00 nmethod 6220 0x000000010cda2290 code [0x000000010cda2480, 0x000000010cda2998] + +GC Heap History (20 events): +Event: 0.442 GC heap before +{Heap before GC invocations=1 (full 0): + garbage-first heap total 606208K, used 38051K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 3 young (24576K), 1 survivors (8192K) + Metaspace used 9689K, committed 9856K, reserved 1114112K + class space used 1076K, committed 1152K, reserved 1048576K +} +Event: 0.444 GC heap after +{Heap after GC invocations=2 (full 0): + garbage-first heap total 606208K, used 23419K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 9689K, committed 9856K, reserved 1114112K + class space used 1076K, committed 1152K, reserved 1048576K +} +Event: 0.807 GC heap before +{Heap before GC invocations=2 (full 0): + garbage-first heap total 606208K, used 47995K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 5 young (40960K), 1 survivors (8192K) + Metaspace used 13722K, committed 13888K, reserved 1114112K + class space used 1603K, committed 1664K, reserved 1048576K +} +Event: 0.813 GC heap after +{Heap after GC invocations=3 (full 0): + garbage-first heap total 606208K, used 27477K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 13722K, committed 13888K, reserved 1114112K + class space used 1603K, committed 1664K, reserved 1048576K +} +Event: 1.392 GC heap before +{Heap before GC invocations=3 (full 0): + garbage-first heap total 606208K, used 68437K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 7 young (57344K), 1 survivors (8192K) + Metaspace used 21201K, committed 21504K, reserved 1114112K + class space used 2676K, committed 2816K, reserved 1048576K +} +Event: 1.395 GC heap after +{Heap after GC invocations=4 (full 0): + garbage-first heap total 606208K, used 31119K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 21201K, committed 21504K, reserved 1114112K + class space used 2676K, committed 2816K, reserved 1048576K +} +Event: 2.847 GC heap before +{Heap before GC invocations=5 (full 0): + garbage-first heap total 196608K, used 137615K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 14 young (114688K), 1 survivors (8192K) + Metaspace used 32674K, committed 33088K, reserved 1114112K + class space used 4093K, committed 4288K, reserved 1048576K +} +Event: 2.861 GC heap after +{Heap after GC invocations=6 (full 0): + garbage-first heap total 196608K, used 32785K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 32674K, committed 33088K, reserved 1114112K + class space used 4093K, committed 4288K, reserved 1048576K +} +Event: 2.927 GC heap before +{Heap before GC invocations=6 (full 0): + garbage-first heap total 196608K, used 40977K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 1 survivors (8192K) + Metaspace used 34086K, committed 34432K, reserved 1114112K + class space used 4246K, committed 4416K, reserved 1048576K +} +Event: 2.931 GC heap after +{Heap after GC invocations=7 (full 0): + garbage-first heap total 196608K, used 33192K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 34086K, committed 34432K, reserved 1114112K + class space used 4246K, committed 4416K, reserved 1048576K +} +Event: 3.207 GC heap before +{Heap before GC invocations=7 (full 0): + garbage-first heap total 196608K, used 41384K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 4 young (32768K), 1 survivors (8192K) + Metaspace used 35986K, committed 36288K, reserved 1114112K + class space used 4497K, committed 4672K, reserved 1048576K +} +Event: 3.213 GC heap after +{Heap after GC invocations=8 (full 0): + garbage-first heap total 196608K, used 33908K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 35986K, committed 36288K, reserved 1114112K + class space used 4497K, committed 4672K, reserved 1048576K +} +Event: 4.154 GC heap before +{Heap before GC invocations=9 (full 0): + garbage-first heap total 196608K, used 115828K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 12 young (98304K), 1 survivors (8192K) + Metaspace used 45522K, committed 45952K, reserved 1114112K + class space used 5884K, committed 6080K, reserved 1048576K +} +Event: 4.157 GC heap after +{Heap after GC invocations=10 (full 0): + garbage-first heap total 262144K, used 37091K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 45522K, committed 45952K, reserved 1114112K + class space used 5884K, committed 6080K, reserved 1048576K +} +Event: 4.268 GC heap before +{Heap before GC invocations=10 (full 0): + garbage-first heap total 262144K, used 45283K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 1 survivors (8192K) + Metaspace used 46797K, committed 47168K, reserved 1114112K + class space used 6085K, committed 6272K, reserved 1048576K +} +Event: 4.273 GC heap after +{Heap after GC invocations=11 (full 0): + garbage-first heap total 262144K, used 37205K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 46797K, committed 47168K, reserved 1114112K + class space used 6085K, committed 6272K, reserved 1048576K +} +Event: 6.516 GC heap before +{Heap before GC invocations=11 (full 0): + garbage-first heap total 262144K, used 176469K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 18 young (147456K), 1 survivors (8192K) + Metaspace used 55389K, committed 55808K, reserved 1114112K + class space used 7288K, committed 7488K, reserved 1048576K +} +Event: 6.521 GC heap after +{Heap after GC invocations=12 (full 0): + garbage-first heap total 262144K, used 45059K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 2 survivors (16384K) + Metaspace used 55389K, committed 55808K, reserved 1114112K + class space used 7288K, committed 7488K, reserved 1048576K +} +Event: 8.053 GC heap before +{Heap before GC invocations=12 (full 0): + garbage-first heap total 262144K, used 135171K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 15 young (122880K), 2 survivors (16384K) + Metaspace used 60370K, committed 60800K, reserved 1114112K + class space used 7985K, committed 8192K, reserved 1048576K +} +Event: 8.065 GC heap after +{Heap after GC invocations=13 (full 0): + garbage-first heap total 262144K, used 51496K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 3 young (24576K), 3 survivors (24576K) + Metaspace used 60370K, committed 60800K, reserved 1114112K + class space used 7985K, committed 8192K, reserved 1048576K +} + +Dll operation events (11 events): +Event: 0.007 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjava.dylib +Event: 0.007 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +Event: 0.078 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libinstrument.dylib +Event: 0.081 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnio.dylib +Event: 0.084 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnet.dylib +Event: 0.124 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjimage.dylib +Event: 0.134 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +Event: 0.211 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement.dylib +Event: 0.216 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement_ext.dylib +Event: 0.319 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libextnet.dylib +Event: 6.751 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libverify.dylib + +Deoptimization events (20 events): +Event: 9.084 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56ea28 sp=0x000000016f37db60 +Event: 9.084 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d800 mode 1 +Event: 9.084 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56dcbc sp=0x000000016f37dc30 +Event: 9.084 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d900 mode 1 +Event: 9.085 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56ea28 sp=0x000000016f37db60 +Event: 9.085 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d800 mode 1 +Event: 9.085 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56dcbc sp=0x000000016f37dc30 +Event: 9.085 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d900 mode 1 +Event: 9.104 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56ea28 sp=0x000000016f37db80 +Event: 9.104 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d820 mode 1 +Event: 9.104 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56dcbc sp=0x000000016f37dc50 +Event: 9.104 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d920 mode 1 +Event: 9.105 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56ea28 sp=0x000000016f37db60 +Event: 9.105 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d800 mode 1 +Event: 9.105 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56dcbc sp=0x000000016f37dc30 +Event: 9.105 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d900 mode 1 +Event: 9.106 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56ea28 sp=0x000000016f37db80 +Event: 9.106 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d820 mode 1 +Event: 9.106 Thread 0x000000010b808a00 DEOPT PACKING pc=0x000000010c56dcbc sp=0x000000016f37dc50 +Event: 9.106 Thread 0x000000010b808a00 DEOPT UNPACKING pc=0x000000010c14777c sp=0x000000016f37d920 mode 1 + +Classes unloaded (0 events): +No events + +Classes redefined (1 events): +Event: 0.114 Thread 0x000000012b8059d0 redefined class name=java.lang.Throwable, count=1 + +Internal exceptions (20 events): +Event: 6.369 Thread 0x000000010b808a00 Exception (0x00000005c61a56a8) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 6.370 Thread 0x000000010b808a00 Exception (0x00000005c61ac0f8) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 6.371 Thread 0x000000010b808a00 Exception (0x00000005c61b00f0) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 6.388 Thread 0x000000012b45ba00 Exception (0x00000005c7a137e0) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 6.404 Thread 0x000000010b808a00 Exception (0x00000005c6294068) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 6.891 Thread 0x000000012b45ba00 Exception (0x00000005cbfd3b50) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 7.393 Thread 0x000000012b45ba00 Exception (0x00000005cbfe0578) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 7.899 Thread 0x000000012b45ba00 Exception (0x00000005c9409f48) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 8.401 Thread 0x000000012b45ba00 Exception (0x00000005e02167a0) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 8.927 Thread 0x000000012b45ba00 Exception (0x00000005ccc28338) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 8.948 Thread 0x000000010b808a00 Exception (0x00000005ccc21b10) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 8.948 Thread 0x000000010b808a00 Exception (0x00000005ccc26808) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 8.949 Thread 0x000000010b808a00 Exception (0x00000005ccc3f340) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 8.949 Thread 0x000000010b808a00 Exception (0x00000005ccc4b0d8) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 9.084 Thread 0x000000010b808a00 Exception (0x00000005cc19f8f0) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 9.085 Thread 0x000000010b808a00 Exception (0x00000005cc1a9e48) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 9.105 Thread 0x000000010b808a00 Exception (0x00000005cc24dc60) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 9.105 Thread 0x000000010b808a00 Exception (0x00000005cc2577d0) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 9.106 Thread 0x000000010b808a00 Exception (0x00000005cc2624f8) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 9.126 Thread 0x000000011ebea000 Exception (0x00000005cc5769a0) +thrown [src/hotspot/share/prims/jni.cpp, line 516] + +VM Operations (20 events): +Event: 8.053 Executing VM operation: CollectForMetadataAllocation +Event: 8.070 Executing VM operation: CollectForMetadataAllocation done +Event: 8.080 Executing VM operation: G1PauseRemark +Event: 8.084 Executing VM operation: G1PauseRemark done +Event: 8.088 Executing VM operation: G1PauseCleanup +Event: 8.088 Executing VM operation: G1PauseCleanup done +Event: 8.450 Executing VM operation: HandshakeAllThreads +Event: 8.450 Executing VM operation: HandshakeAllThreads done +Event: 8.461 Executing VM operation: HandshakeAllThreads +Event: 8.462 Executing VM operation: HandshakeAllThreads done +Event: 8.462 Executing VM operation: HandshakeAllThreads +Event: 8.462 Executing VM operation: HandshakeAllThreads done +Event: 8.957 Executing VM operation: HandshakeAllThreads +Event: 8.957 Executing VM operation: HandshakeAllThreads done +Event: 8.964 Executing VM operation: HandshakeAllThreads +Event: 8.964 Executing VM operation: HandshakeAllThreads done +Event: 8.980 Executing VM operation: HandshakeAllThreads +Event: 8.980 Executing VM operation: HandshakeAllThreads done +Event: 9.027 Executing VM operation: ICBufferFull +Event: 9.033 Executing VM operation: ICBufferFull done + +Events (20 events): +Event: 9.126 loading class java/net/SocksSocketImpl$3 done +Event: 9.126 loading class sun/net/util/SocketExceptions +Event: 9.126 loading class sun/net/util/SocketExceptions done +Event: 9.127 Thread 0x000000011f24c800 Thread added: 0x000000011f24c800 +Event: 9.127 Protecting memory [0x000000032ab00000,0x000000032ab0c000] with protection modes 0 +Event: 9.127 loading class java/lang/Throwable$WrappedPrintWriter +Event: 9.127 loading class java/lang/Throwable$WrappedPrintWriter done +Event: 9.128 loading class com/intellij/rt/debugger/agent/CaptureStorage$9 +Event: 9.128 loading class com/intellij/rt/debugger/agent/CaptureStorage$Callable +Event: 9.128 loading class com/intellij/rt/debugger/agent/CaptureStorage$Callable done +Event: 9.128 loading class com/intellij/rt/debugger/agent/CaptureStorage$9 done +Event: 9.128 loading class jdk/internal/loader/BootLoader$PackageHelper$1 +Event: 9.128 loading class jdk/internal/loader/BootLoader$PackageHelper$1 done +Event: 9.128 loading class jdk/internal/loader/BootLoader$PackageHelper$2 +Event: 9.128 loading class jdk/internal/loader/BootLoader$PackageHelper$2 done +Event: 9.128 loading class java/util/jar/JarInputStream +Event: 9.128 loading class java/util/zip/ZipInputStream +Event: 9.129 loading class java/util/zip/ZipInputStream done +Event: 9.129 loading class java/util/jar/JarInputStream done +Event: 9.129 loading class com/intellij/rt/debugger/agent/CaptureStorage$StackData + + +Dynamic libraries: +0x0000000100d44000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjli.dylib +0x0000000196c18000 /usr/lib/libz.1.dylib +0x0000000196cce000 /usr/lib/libSystem.B.dylib +0x0000000196cc8000 /usr/lib/system/libcache.dylib +0x0000000196c83000 /usr/lib/system/libcommonCrypto.dylib +0x0000000196cae000 /usr/lib/system/libcompiler_rt.dylib +0x0000000196ca3000 /usr/lib/system/libcopyfile.dylib +0x0000000186bb6000 /usr/lib/system/libcorecrypto.dylib +0x0000000186cb6000 /usr/lib/system/libdispatch.dylib +0x0000000186a53000 /usr/lib/system/libdyld.dylib +0x0000000196cbe000 /usr/lib/system/libkeymgr.dylib +0x0000000196c66000 /usr/lib/system/libmacho.dylib +0x0000000195ef9000 /usr/lib/system/libquarantine.dylib +0x0000000196cbb000 /usr/lib/system/libremovefile.dylib +0x000000018d629000 /usr/lib/system/libsystem_asl.dylib +0x0000000186b3c000 /usr/lib/system/libsystem_blocks.dylib +0x0000000186d01000 /usr/lib/system/libsystem_c.dylib +0x0000000196cb2000 /usr/lib/system/libsystem_collections.dylib +0x0000000194899000 /usr/lib/system/libsystem_configuration.dylib +0x0000000193487000 /usr/lib/system/libsystem_containermanager.dylib +0x0000000196698000 /usr/lib/system/libsystem_coreservices.dylib +0x000000018ae50000 /usr/lib/system/libsystem_darwin.dylib +0x000000028c8a4000 /usr/lib/system/libsystem_darwindirectory.dylib +0x0000000196cbf000 /usr/lib/system/libsystem_dnssd.dylib +0x000000028c8a8000 /usr/lib/system/libsystem_eligibility.dylib +0x0000000186cfe000 /usr/lib/system/libsystem_featureflags.dylib +0x0000000186e83000 /usr/lib/system/libsystem_info.dylib +0x0000000196c27000 /usr/lib/system/libsystem_m.dylib +0x0000000186c65000 /usr/lib/system/libsystem_malloc.dylib +0x000000018d58c000 /usr/lib/system/libsystem_networkextension.dylib +0x000000018b2bb000 /usr/lib/system/libsystem_notify.dylib +0x000000019489e000 /usr/lib/system/libsystem_sandbox.dylib +0x000000028c8b3000 /usr/lib/system/libsystem_sanitizers.dylib +0x0000000196cb7000 /usr/lib/system/libsystem_secinit.dylib +0x0000000186e2f000 /usr/lib/system/libsystem_kernel.dylib +0x0000000186e7a000 /usr/lib/system/libsystem_platform.dylib +0x0000000186e6d000 /usr/lib/system/libsystem_pthread.dylib +0x000000018f1e2000 /usr/lib/system/libsystem_symptoms.dylib +0x0000000186b95000 /usr/lib/system/libsystem_trace.dylib +0x000000028c8bb000 /usr/lib/system/libsystem_trial.dylib +0x0000000196c91000 /usr/lib/system/libunwind.dylib +0x0000000186b40000 /usr/lib/system/libxpc.dylib +0x0000000186a00000 /usr/lib/libobjc.A.dylib +0x0000000186eb3000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation +0x000000019a5c3000 /usr/lib/swift/libswiftCore.dylib +0x0000000186e14000 /usr/lib/libc++abi.dylib +0x000000028ac91000 /usr/lib/libRosetta.dylib +0x0000000186d83000 /usr/lib/libc++.1.dylib +0x0000000188722000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation +0x00000001a41a3000 /usr/lib/swift/libswiftObjectiveC.dylib +0x000000028c10d000 /usr/lib/libswiftPrespecialized.dylib +0x0000000188391000 /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration +0x0000000191703000 /System/Library/PrivateFrameworks/CoreAutoLayout.framework/Versions/A/CoreAutoLayout +0x0000000196cd0000 /usr/lib/libfakelink.dylib +0x0000000196f79000 /usr/lib/libcompression.dylib +0x000000018d1d6000 /System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork +0x0000000190b34000 /System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration +0x0000000196d23000 /usr/lib/libarchive.2.dylib +0x0000000190a39000 /usr/lib/libDiagnosticMessagesClient.dylib +0x000000018ab7a000 /usr/lib/libicucore.A.dylib +0x000000019174c000 /usr/lib/libxml2.2.dylib +0x000000019f452000 /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices +0x00000001948ac000 /usr/lib/liblangid.dylib +0x000000018b1d2000 /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit +0x000000019d0c4000 /System/Library/Frameworks/Combine.framework/Versions/A/Combine +0x000000023fff3000 /System/Library/PrivateFrameworks/CollectionsInternal.framework/Versions/A/CollectionsInternal +0x000000026c039000 /System/Library/PrivateFrameworks/ReflectionInternal.framework/Versions/A/ReflectionInternal +0x000000026cf8d000 /System/Library/PrivateFrameworks/RuntimeInternal.framework/Versions/A/RuntimeInternal +0x0000000196cd2000 /System/Library/PrivateFrameworks/SoftLinking.framework/Versions/A/SoftLinking +0x00000001b488c000 /usr/lib/swift/libswiftCoreFoundation.dylib +0x00000001b164f000 /usr/lib/swift/libswiftDarwin.dylib +0x00000001a11c9000 /usr/lib/swift/libswiftDispatch.dylib +0x00000001b48ed000 /usr/lib/swift/libswiftIOKit.dylib +0x000000028c550000 /usr/lib/swift/libswiftSystem.dylib +0x00000001b489f000 /usr/lib/swift/libswiftXPC.dylib +0x000000028c582000 /usr/lib/swift/libswift_Builtin_float.dylib +0x000000028c583000 /usr/lib/swift/libswift_Concurrency.dylib +0x000000028c60f000 /usr/lib/swift/libswift_DarwinFoundation1.dylib +0x000000028c6b3000 /usr/lib/swift/libswift_StringProcessing.dylib +0x00000001a41a7000 /usr/lib/swift/libswiftos.dylib +0x000000018b152000 /System/Library/PrivateFrameworks/CoreServicesInternal.framework/Versions/A/CoreServicesInternal +0x0000000196c9b000 /usr/lib/liboah.dylib +0x000000018a75a000 /System/Library/Frameworks/Security.framework/Versions/A/Security +0x00000001a35d7000 /System/Library/PrivateFrameworks/DiskImages.framework/Versions/A/DiskImages +0x00000001b10f3000 /System/Library/Frameworks/NetFS.framework/Versions/A/NetFS +0x00000001916c8000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents +0x000000018ae5a000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore +0x0000000190aa8000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata +0x000000019669f000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices +0x0000000196e1b000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit +0x000000018f15c000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE +0x0000000187412000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices +0x0000000198224000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices +0x00000001916d5000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList +0x0000000196eae000 /usr/lib/libapple_nghttp2.dylib +0x000000018ed78000 /usr/lib/libsqlite3.dylib +0x000000018ef61000 /System/Library/Frameworks/Accounts.framework/Versions/A/Accounts +0x00000001a3819000 /System/Library/PrivateFrameworks/AppSupport.framework/Versions/A/AppSupport +0x00000001b363e000 /System/Library/Frameworks/AVFoundation.framework/Versions/A/AVFoundation +0x0000000190a09000 /System/Library/PrivateFrameworks/CoreAnalytics.framework/Versions/A/CoreAnalytics +0x000000018dc1c000 /System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics +0x000000019b2fe000 /System/Library/Frameworks/GSS.framework/Versions/A/GSS +0x00000001996e6000 /System/Library/PrivateFrameworks/InternationalSupport.framework/Versions/A/InternationalSupport +0x000000018f0f0000 /System/Library/PrivateFrameworks/RunningBoardServices.framework/Versions/A/RunningBoardServices +0x00000001a412b000 /System/Library/PrivateFrameworks/StreamingZip.framework/Versions/A/StreamingZip +0x000000018d5a7000 /usr/lib/libenergytrace.dylib +0x000000018f1eb000 /System/Library/Frameworks/Network.framework/Versions/A/Network +0x0000000195f21000 /usr/lib/libbsm.0.dylib +0x0000000196c6a000 /usr/lib/system/libkxld.dylib +0x000000023b5c5000 /System/Library/PrivateFrameworks/AppleKeyStore.framework/Versions/A/AppleKeyStore +0x000000028a993000 /usr/lib/libCoreEntitlements.dylib +0x0000000260705000 /System/Library/PrivateFrameworks/MessageSecurity.framework/Versions/A/MessageSecurity +0x000000018ed5c000 /System/Library/PrivateFrameworks/ProtocolBuffer.framework/Versions/A/ProtocolBuffer +0x00000001a05d8000 /System/Library/PrivateFrameworks/SymptomDiagnosticReporter.framework/Versions/A/SymptomDiagnosticReporter +0x0000000198469000 /System/Library/PrivateFrameworks/CrashReporterSupport.framework/Versions/A/CrashReporterSupport +0x000000018d5a9000 /usr/lib/libMobileGestalt.dylib +0x000000019667f000 /System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression +0x0000000195f09000 /usr/lib/libcoretls.dylib +0x000000019829a000 /usr/lib/libcoretls_cfhelpers.dylib +0x0000000196f73000 /usr/lib/libpam.2.dylib +0x0000000198310000 /usr/lib/libxar.1.dylib +0x000000019829c000 /System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS +0x0000000278713000 /System/Library/PrivateFrameworks/SwiftASN1Internal.framework/Versions/A/SwiftASN1Internal +0x000000019831f000 /usr/lib/libutil.dylib +0x00000001948a7000 /System/Library/PrivateFrameworks/AppleSystemInfo.framework/Versions/A/AppleSystemInfo +0x0000000195bd0000 /System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/Versions/A/IOMobileFramebuffer +0x00000001934c0000 /System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface +0x00000001a2f37000 /System/Library/PrivateFrameworks/CoreWiFi.framework/Versions/A/CoreWiFi +0x00000001b474c000 /System/Library/PrivateFrameworks/LoggingSupport.framework/Versions/A/LoggingSupport +0x000000019b361000 /System/Library/PrivateFrameworks/MobileAsset.framework/Versions/A/MobileAsset +0x00000001a05e8000 /System/Library/PrivateFrameworks/PowerLog.framework/Versions/A/PowerLog +0x00000001a1aaa000 /System/Library/PrivateFrameworks/Rapport.framework/Versions/A/Rapport +0x000000023416e000 /System/Library/Frameworks/SwiftData.framework/Versions/A/SwiftData +0x000000018cc0a000 /System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers +0x00000001918f5000 /System/Library/PrivateFrameworks/UserManagement.framework/Versions/A/UserManagement +0x000000018d0fd000 /usr/lib/libboringssl.dylib +0x000000018f1d0000 /usr/lib/libdns_services.dylib +0x00000001b3772000 /usr/lib/libquic.dylib +0x000000019a554000 /usr/lib/libusrtcp.dylib +0x000000023c47f000 /System/Library/PrivateFrameworks/AtomicsInternal.framework/Versions/A/AtomicsInternal +0x00000001dab32000 /System/Library/PrivateFrameworks/InternalSwiftProtobuf.framework/Versions/A/InternalSwiftProtobuf +0x000000028c3d7000 /usr/lib/swift/libswiftDistributed.dylib +0x000000028c400000 /usr/lib/swift/libswiftObservation.dylib +0x000000028c53c000 /usr/lib/swift/libswiftSynchronization.dylib +0x00000001948a5000 /System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary +0x000000023ccaf000 /System/Library/PrivateFrameworks/BiomeLibrary.framework/Versions/A/BiomeLibrary +0x00000001c38d5000 /System/Library/PrivateFrameworks/BiomeStreams.framework/Versions/A/BiomeStreams +0x00000001bf924000 /System/Library/PrivateFrameworks/BiomeFoundation.framework/Versions/A/BiomeFoundation +0x00000001c9b82000 /System/Library/PrivateFrameworks/BiomePubSub.framework/Versions/A/BiomePubSub +0x000000018e96e000 /System/Library/Frameworks/CoreData.framework/Versions/A/CoreData +0x00000001a510e000 /System/Library/PrivateFrameworks/ProactiveSupport.framework/Versions/A/ProactiveSupport +0x00000002361cc000 /System/Library/Frameworks/_LocationEssentials.framework/Versions/A/_LocationEssentials +0x000000019827b000 /usr/lib/liblzma.5.dylib +0x000000019f6d1000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate +0x0000000195e02000 /System/Library/PrivateFrameworks/MobileKeyBag.framework/Versions/A/MobileKeyBag +0x00000001a3b2d000 /System/Library/PrivateFrameworks/InternationalTextSearch.framework/Versions/A/InternationalTextSearch +0x00000001bbdf7000 /System/Library/PrivateFrameworks/SoftwareUpdateCoreSupport.framework/Versions/A/SoftwareUpdateCoreSupport +0x00000001c4374000 /System/Library/PrivateFrameworks/SoftwareUpdateCoreConnect.framework/Versions/A/SoftwareUpdateCoreConnect +0x00000001a36d1000 /System/Library/PrivateFrameworks/RemoteServiceDiscovery.framework/Versions/A/RemoteServiceDiscovery +0x00000001bb9ce000 /System/Library/PrivateFrameworks/MSUDataAccessor.framework/Versions/A/MSUDataAccessor +0x00000001b691f000 /usr/lib/libbootpolicy.dylib +0x00000001a36e8000 /System/Library/PrivateFrameworks/RemoteXPC.framework/Versions/A/RemoteXPC +0x00000001c37a9000 /usr/lib/libFDR.dylib +0x00000001c9784000 /usr/lib/libamsupport.dylib +0x000000028ac89000 /usr/lib/libReverseProxyDevice.dylib +0x000000023ae33000 /System/Library/PrivateFrameworks/AppleDeviceQuerySupport.framework/Versions/A/AppleDeviceQuerySupport +0x00000001cc94e000 /usr/lib/libpartition2_dynamic.dylib +0x0000000196e8a000 /System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce +0x000000028a83e000 /usr/lib/libAppleArchive.dylib +0x000000019668b000 /usr/lib/libbz2.1.0.dylib +0x0000000190b3e000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage +0x000000019f42d000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib +0x0000000198356000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib +0x0000000187916000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib +0x00000001a3b2c000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices +0x0000000191833000 /System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo +0x000000018e372000 /System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync +0x0000000189d9a000 /System/Library/Frameworks/CoreText.framework/Versions/A/CoreText +0x0000000193fb3000 /System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO +0x000000019af0e000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS +0x000000018e51a000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices +0x00000001995dc000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore +0x000000019b2c7000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD +0x000000019b2c2000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy +0x000000019aee0000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis +0x000000018d665000 /System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight +0x000000019398e000 /System/Library/PrivateFrameworks/FontServices.framework/libFontParser.dylib +0x000000018effe000 /System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard +0x00000001a152a000 /System/Library/PrivateFrameworks/BoardServices.framework/Versions/A/BoardServices +0x00000001a33f9000 /System/Library/PrivateFrameworks/BackBoardServices.framework/Versions/A/BackBoardServices +0x000000023cbb4000 /System/Library/PrivateFrameworks/BackBoardHIDEventFoundation.framework/Versions/A/BackBoardHIDEventFoundation +0x00000001898b6000 /System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay +0x0000000198f35000 /System/Library/Frameworks/VideoToolbox.framework/Versions/A/VideoToolbox +0x0000000196f71000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders +0x000000026af43000 /System/Library/PrivateFrameworks/ProDisplayLibrary.framework/Versions/A/ProDisplayLibrary +0x00000001a722e000 /System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/Versions/A/IOSurfaceAccelerator +0x00000001934e8000 /System/Library/Frameworks/Metal.framework/Versions/A/Metal +0x00000001934dd000 /System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator +0x00000001937ec000 /System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia +0x000000018d641000 /System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC +0x0000000198eed000 /System/Library/PrivateFrameworks/WatchdogClient.framework/Versions/A/WatchdogClient +0x0000000190f63000 /System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore +0x0000000198eef000 /System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport +0x00000001cc730000 /usr/lib/swift/libswiftAccelerate.dylib +0x00000001b486c000 /usr/lib/swift/libswiftCoreAudio.dylib +0x00000001d08c1000 /usr/lib/swift/libswiftCoreMedia.dylib +0x00000001c2862000 /usr/lib/swift/libswiftMetal.dylib +0x00000001d2074000 /usr/lib/swift/libswiftOSLog.dylib +0x00000001c7c88000 /usr/lib/swift/libswiftQuartzCore.dylib +0x00000001cc720000 /usr/lib/swift/libswiftUniformTypeIdentifiers.dylib +0x000000028c56a000 /usr/lib/swift/libswiftVideoToolbox.dylib +0x00000001b83e6000 /usr/lib/swift/libswiftsimd.dylib +0x00000001c9be3000 /System/Library/PrivateFrameworks/BiomeStorage.framework/Versions/A/BiomeStorage +0x00000002593f4000 /System/Library/PrivateFrameworks/IntelligencePlatformLibrary.framework/Versions/A/IntelligencePlatformLibrary +0x0000000269d07000 /System/Library/PrivateFrameworks/PoirotSchematizer.framework/Versions/A/PoirotSchematizer +0x000000023d56a000 /System/Library/PrivateFrameworks/BiomeSync.framework/Versions/A/BiomeSync +0x000000023cc95000 /System/Library/PrivateFrameworks/BiomeDSL.framework/Versions/A/BiomeDSL +0x00000001e2d3d000 /System/Library/PrivateFrameworks/FeatureFlags.framework/Versions/A/FeatureFlags +0x0000000269d75000 /System/Library/PrivateFrameworks/PoirotUDFs.framework/Versions/A/PoirotUDFs +0x000000028c612000 /usr/lib/swift/libswift_DarwinFoundation2.dylib +0x000000028c613000 /usr/lib/swift/libswift_DarwinFoundation3.dylib +0x00000001a1a9f000 /System/Library/PrivateFrameworks/CoreTime.framework/Versions/A/CoreTime +0x0000000196d08000 /usr/lib/libiconv.2.dylib +0x0000000196c65000 /usr/lib/libcharset.1.dylib +0x0000000269cca000 /System/Library/PrivateFrameworks/PoirotSQLite.framework/Versions/A/PoirotSQLite +0x000000028c614000 /usr/lib/swift/libswift_RegexParser.dylib +0x000000023eba0000 /System/Library/PrivateFrameworks/CascadeSets.framework/Versions/A/CascadeSets +0x000000019b4d8000 /System/Library/PrivateFrameworks/CorePhoneNumbers.framework/Versions/A/CorePhoneNumbers +0x0000000198ce7000 /System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG +0x00000001986c0000 /usr/lib/libexpat.1.dylib +0x00000001994b2000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib +0x00000001994dd000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib +0x00000001995c5000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib +0x0000000198d2c000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib +0x00000001983d0000 /usr/lib/libate.dylib +0x000000019956c000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib +0x0000000199563000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib +0x000000024f044000 /System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/32023/Libraries/libllvm-flatbuffers.dylib +0x0000000249aa3000 /System/Library/PrivateFrameworks/FramePacing.framework/Versions/A/FramePacing +0x000000022cbb3000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib +0x000000024abd1000 /System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/32023/Libraries/libGPUCompilerUtils.dylib +0x00000001a15f7000 /System/Library/PrivateFrameworks/GraphicsServices.framework/Versions/A/GraphicsServices +0x000000022cbc1000 /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL +0x000000022cc12000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib +0x000000022cbd5000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib +0x000000022cda2000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib +0x000000022cbde000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib +0x000000022cbd2000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib +0x000000022cbbb000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib +0x000000019955e000 /System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler +0x000000019953e000 /System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment +0x0000000199566000 /System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay +0x00000002805c9000 /System/Library/PrivateFrameworks/VideoToolboxParavirtualizationSupport.framework/Versions/A/VideoToolboxParavirtualizationSupport +0x0000000198677000 /System/Library/PrivateFrameworks/AppleVA.framework/Versions/A/AppleVA +0x000000022ec3e000 /System/Library/Frameworks/ExtensionFoundation.framework/Versions/A/ExtensionFoundation +0x00000001995cb000 /System/Library/PrivateFrameworks/CMCaptureCore.framework/Versions/A/CMCaptureCore +0x0000000198951000 /usr/lib/libspindump.dylib +0x0000000189fc4000 /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio +0x0000000198944000 /System/Library/PrivateFrameworks/AppServerSupport.framework/Versions/A/AppServerSupport +0x000000019b2d0000 /System/Library/PrivateFrameworks/perfdata.framework/Versions/A/perfdata +0x00000001899d7000 /System/Library/PrivateFrameworks/AudioToolboxCore.framework/Versions/A/AudioToolboxCore +0x00000001937c2000 /System/Library/PrivateFrameworks/caulk.framework/Versions/A/caulk +0x000000019aec6000 /usr/lib/libAudioStatistics.dylib +0x00000001b3867000 /System/Library/PrivateFrameworks/SystemPolicy.framework/Versions/A/SystemPolicy +0x000000019b174000 /usr/lib/libSMC.dylib +0x00000001bb1dd000 /usr/lib/swift/libswiftCoreMIDI.dylib +0x00000001a651d000 /System/Library/Frameworks/CoreMIDI.framework/Versions/A/CoreMIDI +0x000000019948c000 /usr/lib/libAudioToolboxUtility.dylib +0x000000019b2de000 /usr/lib/libperfcheck.dylib +0x000000023c54e000 /System/Library/PrivateFrameworks/AudioAnalytics.framework/Versions/A/AudioAnalytics +0x00000001da81e000 /System/Library/Frameworks/OSLog.framework/Versions/A/OSLog +0x0000000265777000 /System/Library/PrivateFrameworks/OSEligibility.framework/Versions/A/OSEligibility +0x0000000198746000 /System/Library/PrivateFrameworks/IconServices.framework/Versions/A/IconServices +0x0000000230025000 /System/Library/Frameworks/LightweightCodeRequirements.framework/Versions/A/LightweightCodeRequirements +0x00000001985c0000 /System/Library/PrivateFrameworks/PlugInKit.framework/Versions/A/PlugInKit +0x0000000195e1a000 /System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices +0x00000001986e5000 /System/Library/PrivateFrameworks/IconFoundation.framework/Versions/A/IconFoundation +0x0000000255f80000 /System/Library/PrivateFrameworks/IconRendering.framework/Versions/A/IconRendering +0x00000001913ca000 /System/Library/PrivateFrameworks/CoreUI.framework/Versions/A/CoreUI +0x00000001942f4000 /System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage +0x000000026d172000 /System/Library/PrivateFrameworks/SFSymbols.framework/Versions/A/SFSymbols +0x000000022eaf4000 /System/Library/Frameworks/DeveloperToolsSupport.framework/Versions/A/DeveloperToolsSupport +0x00000001ab7ca000 /System/Library/PrivateFrameworks/RenderBox.framework/Versions/A/RenderBox +0x0000000193f75000 /System/Library/PrivateFrameworks/CoreSVG.framework/Versions/A/CoreSVG +0x000000019964f000 /System/Library/PrivateFrameworks/TextureIO.framework/Versions/A/TextureIO +0x00000001b48ec000 /usr/lib/swift/libswiftCoreImage.dylib +0x00000001988f4000 /System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer +0x00000002499ae000 /System/Library/PrivateFrameworks/FontServices.framework/Versions/A/FontServices +0x0000000198904000 /System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG +0x0000000191379000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib +0x000000028b763000 /usr/lib/libhvf.dylib +0x0000000266404000 /System/Library/PrivateFrameworks/ParsingInternal.framework/Versions/A/ParsingInternal +0x00000002499b2000 /System/Library/PrivateFrameworks/FontServices.framework/libXTFontStaticRegistryData.dylib +0x00000001947df000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSCore.framework/Versions/A/MPSCore +0x00000001965ea000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSImage.framework/Versions/A/MPSImage +0x0000000195fa9000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork +0x00000001963e8000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix +0x0000000196200000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector +0x000000019641a000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNDArray.framework/Versions/A/MPSNDArray +0x0000000230eaa000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSFunctions.framework/Versions/A/MPSFunctions +0x0000000230e8b000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSBenchmarkLoop.framework/Versions/A/MPSBenchmarkLoop +0x0000000230ebe000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSHost.framework/Versions/A/MPSHost +0x000000018772d000 /System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools +0x00000001b9b0d000 /System/Library/PrivateFrameworks/IOAccelMemoryInfo.framework/Versions/A/IOAccelMemoryInfo +0x00000001c807b000 /System/Library/PrivateFrameworks/kperf.framework/Versions/A/kperf +0x00000001b4868000 /System/Library/PrivateFrameworks/GPURawCounter.framework/Versions/A/GPURawCounter +0x00000001a5299000 /System/Library/PrivateFrameworks/ASEProcessing.framework/Versions/A/ASEProcessing +0x00000001d618a000 /System/Library/PrivateFrameworks/Symbolication.framework/Versions/A/Symbolication +0x00000002698b5000 /System/Library/PrivateFrameworks/PhotosensitivityProcessing.framework/Versions/A/PhotosensitivityProcessing +0x000000026d1f8000 /System/Library/PrivateFrameworks/SILManager.framework/Versions/A/SILManager +0x00000001a1943000 /System/Library/PrivateFrameworks/CoreSymbolication.framework/Versions/A/CoreSymbolication +0x00000001b47db000 /System/Library/PrivateFrameworks/MallocStackLogging.framework/Versions/A/MallocStackLogging +0x00000001a1921000 /System/Library/PrivateFrameworks/DebugSymbols.framework/Versions/A/DebugSymbols +0x00000001c67cc000 /System/Library/PrivateFrameworks/OSAnalytics.framework/Versions/A/OSAnalytics +0x0000000247709000 /System/Library/PrivateFrameworks/DeviceRecovery.framework/Versions/A/DeviceRecovery +0x000000027be51000 /System/Library/PrivateFrameworks/Tightbeam.framework/Versions/A/Tightbeam +0x00000001c2870000 /usr/lib/swift/libswiftCompression.dylib +0x00000001ccebd000 /System/Library/PrivateFrameworks/AFKUser.framework/Versions/A/AFKUser +0x0000000199597000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATSUI.framework/Versions/A/ATSUI +0x000000019ac6f000 /System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox +0x0000000196a5c000 /System/Library/Frameworks/UserNotifications.framework/Versions/A/UserNotifications +0x00000001ba2a4000 /System/Library/PrivateFrameworks/SiriInstrumentation.framework/Versions/A/SiriInstrumentation +0x000000026f688000 /System/Library/PrivateFrameworks/SiriAnalytics.framework/Versions/A/SiriAnalytics +0x00000001b7a03000 /System/Library/PrivateFrameworks/FeedbackLogger.framework/Versions/A/FeedbackLogger +0x00000001d230d000 /usr/lib/swift/libswiftAVFoundation.dylib +0x000000027e67a000 /System/Library/PrivateFrameworks/UnifiedAssetFramework.framework/Versions/A/UnifiedAssetFramework +0x000000019ae45000 /System/Library/PrivateFrameworks/AudioSession.framework/Versions/A/AudioSession +0x0000000198805000 /System/Library/PrivateFrameworks/MediaExperience.framework/Versions/A/MediaExperience +0x000000019ac19000 /System/Library/PrivateFrameworks/AudioSession.framework/libSessionUtility.dylib +0x00000001a04cd000 /System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth +0x0000000195c8d000 /System/Library/PrivateFrameworks/CoreUtils.framework/Versions/A/CoreUtils +0x00000001ac4fe000 /System/Library/PrivateFrameworks/HID.framework/Versions/A/HID +0x00000002465fa000 /System/Library/PrivateFrameworks/CoreUtilsExtras.framework/Versions/A/CoreUtilsExtras +0x0000000255ed5000 /System/Library/PrivateFrameworks/IO80211.framework/Versions/A/IO80211 +0x000000019ce10000 /System/Library/Frameworks/IOBluetooth.framework/Versions/A/IOBluetooth +0x000000028c41f000 /usr/lib/swift/libswiftRegexBuilder.dylib +0x0000000198460000 /usr/lib/libIOReport.dylib +0x00000001e2dc2000 /System/Library/PrivateFrameworks/WiFiPeerToPeer.framework/Versions/A/WiFiPeerToPeer +0x0000000195e29000 /System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation +0x000000023ec54000 /System/Library/PrivateFrameworks/Centauri.framework/Versions/A/Centauri +0x0000000188111000 /System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon +0x000000028bbb3000 /usr/lib/libmrc.dylib +0x0000000255f40000 /System/Library/PrivateFrameworks/IPConfiguration.framework/Versions/A/IPConfiguration +0x00000001d6961000 /System/Library/PrivateFrameworks/Netrb.framework/Versions/A/Netrb +0x00000001a1450000 /System/Library/PrivateFrameworks/FrontBoardServices.framework/Versions/A/FrontBoardServices +0x0000000195f92000 /usr/lib/libgermantok.dylib +0x00000001949ce000 /System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData +0x00000001a06d6000 /System/Library/PrivateFrameworks/MediaKit.framework/Versions/A/MediaKit +0x00000001a0624000 /System/Library/Frameworks/DiscRecording.framework/Versions/A/DiscRecording +0x00000001986db000 /usr/lib/libheimdal-asn1.dylib +0x00000001a4101000 /System/Library/Frameworks/AudioUnit.framework/Versions/A/AudioUnit +0x0000000191690000 /System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory +0x000000019169e000 /System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory +0x000000019d1b8000 /System/Library/PrivateFrameworks/GeoServices.framework/Versions/A/GeoServices +0x000000019abdf000 /System/Library/PrivateFrameworks/LocationSupport.framework/Versions/A/LocationSupport +0x0000000252bfe000 /System/Library/PrivateFrameworks/GeoServicesCore.framework/Versions/A/GeoServicesCore +0x00000001ad402000 /System/Library/PrivateFrameworks/PhoneNumbers.framework/Versions/A/PhoneNumbers +0x000000025b386000 /System/Library/PrivateFrameworks/LocationLogEncryption.framework/Versions/A/LocationLogEncryption +0x000000022d12e000 /System/Library/Frameworks/AVFAudio.framework/Versions/A/AVFAudio +0x000000022d272000 /System/Library/Frameworks/AVRouting.framework/Versions/A/AVRouting +0x00000001ad51a000 /usr/lib/libAccessibility.dylib +0x0000000259d70000 /System/Library/PrivateFrameworks/IsolatedCoreAudioClient.framework/Versions/A/IsolatedCoreAudioClient +0x00000002423ee000 /System/Library/PrivateFrameworks/CoreAudioOrchestration.framework/Versions/A/CoreAudioOrchestration +0x0000000199ab3000 /System/Library/Frameworks/MediaToolbox.framework/Versions/A/MediaToolbox +0x00000001a07fc000 /System/Library/PrivateFrameworks/CoreAVCHD.framework/Versions/A/CoreAVCHD +0x000000019f720000 /System/Library/Frameworks/MediaAccessibility.framework/Versions/A/MediaAccessibility +0x00000001a07f8000 /System/Library/PrivateFrameworks/Mangrove.framework/Versions/A/Mangrove +0x000000023e290000 /System/Library/PrivateFrameworks/CMPhoto.framework/Versions/A/CMPhoto +0x00000001a0fc5000 /System/Library/Frameworks/CoreTelephony.framework/Versions/A/CoreTelephony +0x00000001a07eb000 /System/Library/PrivateFrameworks/CoreAUC.framework/Versions/A/CoreAUC +0x000000023b3f6000 /System/Library/PrivateFrameworks/AppleJPEGXL.framework/Versions/A/AppleJPEGXL +0x000000019b4e8000 /usr/lib/libTelephonyUtilDynamic.dylib +0x00000001dd93f000 /System/Library/Frameworks/CryptoKit.framework/Versions/A/CryptoKit +0x00000001a40fc000 /System/Library/PrivateFrameworks/CryptoKitCBridging.framework/Versions/A/CryptoKitCBridging +0x00000001a1609000 /System/Library/Frameworks/CryptoTokenKit.framework/Versions/A/CryptoTokenKit +0x0000000247179000 /System/Library/PrivateFrameworks/Dendrite.framework/Versions/A/Dendrite +0x00000001b440c000 /System/Library/Frameworks/NaturalLanguage.framework/Versions/A/NaturalLanguage +0x0000000252901000 /System/Library/PrivateFrameworks/GenerativeModels.framework/Versions/A/GenerativeModels +0x00000001e2d49000 /usr/lib/swift/libswiftNaturalLanguage.dylib +0x000000023bf6d000 /System/Library/PrivateFrameworks/AppleMobileFileIntegrity.framework/Versions/A/AppleMobileFileIntegrity +0x000000028ad01000 /usr/lib/libTLE.dylib +0x00000001b480d000 /usr/lib/libmis.dylib +0x00000001ec491000 /System/Library/PrivateFrameworks/ConfigProfileHelper.framework/Versions/A/ConfigProfileHelper +0x00000001a428b000 /System/Library/PrivateFrameworks/Espresso.framework/Versions/A/Espresso +0x0000000191e20000 /System/Library/Frameworks/CoreML.framework/Versions/A/CoreML +0x00000001e0bf5000 /usr/lib/libedit.3.dylib +0x0000000229f3c000 /System/Library/PrivateFrameworks/ANECompiler.framework/Versions/A/ANECompiler +0x00000001a6361000 /System/Library/PrivateFrameworks/AppleNeuralEngine.framework/Versions/A/AppleNeuralEngine +0x000000025b4a4000 /System/Library/PrivateFrameworks/MIL.framework/Versions/A/MIL +0x0000000230ec4000 /System/Library/Frameworks/MetalPerformanceShadersGraph.framework/Versions/A/MetalPerformanceShadersGraph +0x000000025bc13000 /System/Library/PrivateFrameworks/MLCompilerServices.framework/Versions/A/MLCompilerServices +0x00000001a50dd000 /System/Library/PrivateFrameworks/ANEServices.framework/Versions/A/ANEServices +0x00000001b9ad0000 /usr/lib/libncurses.5.4.dylib +0x000000018b2ce000 /usr/lib/libsandbox.1.dylib +0x0000000198601000 /usr/lib/libMatch.1.dylib +0x00000002654f9000 /System/Library/PrivateFrameworks/ODIE.framework/Versions/A/ODIE +0x000000025e8a0000 /System/Library/PrivateFrameworks/MLModelAsset.framework/Versions/A/MLModelAsset +0x000000025bbb9000 /System/Library/PrivateFrameworks/MLCompilerRuntime.framework/Versions/A/MLCompilerRuntime +0x0000000196255000 /System/Library/Frameworks/MLCompute.framework/Versions/A/MLCompute +0x000000025bb3b000 /System/Library/PrivateFrameworks/MLAssetIO.framework/Versions/A/MLAssetIO +0x000000028c3f1000 /usr/lib/swift/libswiftMLCompute.dylib +0x00000001a11e0000 /System/Library/PrivateFrameworks/AVFCore.framework/Versions/A/AVFCore +0x00000001aae1b000 /System/Library/PrivateFrameworks/AVFCapture.framework/Versions/A/AVFCapture +0x000000023e087000 /System/Library/PrivateFrameworks/CMImaging.framework/Versions/A/CMImaging +0x00000001ab05d000 /System/Library/PrivateFrameworks/Quagga.framework/Versions/A/Quagga +0x00000001ab18e000 /System/Library/PrivateFrameworks/CMCapture.framework/Versions/A/CMCapture +0x000000019b071000 /System/Library/Frameworks/CoreMediaIO.framework/Versions/A/CoreMediaIO +0x000000023dfc2000 /System/Library/PrivateFrameworks/CMCaptureDevice.framework/Versions/A/CMCaptureDevice +0x0000000198a3d000 /System/Library/PrivateFrameworks/CoreBrightness.framework/Versions/A/CoreBrightness +0x000000023f14b000 /System/Library/PrivateFrameworks/CinematicFraming.framework/Versions/A/CinematicFraming +0x00000002619dd000 /System/Library/PrivateFrameworks/ModelManagerServices.framework/Versions/A/ModelManagerServices +0x00000001cf375000 /System/Library/PrivateFrameworks/CPMS.framework/Versions/A/CPMS +0x0000000279581000 /System/Library/PrivateFrameworks/SystemStatus.framework/Versions/A/SystemStatus +0x00000001b323c000 /System/Library/Frameworks/CoreMotion.framework/Versions/A/CoreMotion +0x00000001c3854000 /System/Library/PrivateFrameworks/TimeSync.framework/Versions/A/TimeSync +0x0000000247b41000 /System/Library/PrivateFrameworks/DistributedSensing.framework/Versions/A/DistributedSensing +0x00000001bec33000 /System/Library/PrivateFrameworks/MobileBluetooth.framework/Versions/A/MobileBluetooth +0x00000001c5518000 /System/Library/PrivateFrameworks/IOKitten.framework/Versions/A/IOKitten +0x000000023b270000 /System/Library/PrivateFrameworks/AppleIntelligenceReporting.framework/Versions/A/AppleIntelligenceReporting +0x0000000195b9c000 /System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji +0x0000000188425000 /usr/lib/libCRFSuite.dylib +0x0000000189706000 /System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling +0x00000001948ae000 /System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP +0x000000018e683000 /System/Library/PrivateFrameworks/Montreal.framework/Versions/A/Montreal +0x0000000196d10000 /usr/lib/libcmph.dylib +0x0000000195f33000 /usr/lib/libmecab.dylib +0x0000000196e81000 /usr/lib/libThaiTokenizer.dylib +0x00000002529e3000 /System/Library/PrivateFrameworks/GenerativeModelsFoundation.framework/Versions/A/GenerativeModelsFoundation +0x000000027c356000 /System/Library/PrivateFrameworks/TokenGeneration.framework/Versions/A/TokenGeneration +0x00000002527b7000 /System/Library/PrivateFrameworks/GenerativeFunctions.framework/Versions/A/GenerativeFunctions +0x0000000252805000 /System/Library/PrivateFrameworks/GenerativeFunctionsFoundation.framework/Versions/A/GenerativeFunctionsFoundation +0x000000026169e000 /System/Library/PrivateFrameworks/ModelCatalog.framework/Versions/A/ModelCatalog +0x000000026e8a2000 /System/Library/PrivateFrameworks/SensitiveContentAnalysisML.framework/Versions/A/SensitiveContentAnalysisML +0x000000025289b000 /System/Library/PrivateFrameworks/GenerativeFunctionsInstrumentation.framework/Versions/A/GenerativeFunctionsInstrumentation +0x000000026b6cb000 /System/Library/PrivateFrameworks/PromptKit.framework/Versions/A/PromptKit +0x000000026b0e1000 /System/Library/PrivateFrameworks/ProactiveDaemonSupport.framework/Versions/A/ProactiveDaemonSupport +0x000000027c58e000 /System/Library/PrivateFrameworks/TokenGenerationCore.framework/Versions/A/TokenGenerationCore +0x00000001b52db000 /System/Library/PrivateFrameworks/Trial.framework/Versions/A/Trial +0x00000001b525c000 /System/Library/PrivateFrameworks/TrialProto.framework/Versions/A/TrialProto +0x000000023ae9a000 /System/Library/PrivateFrameworks/AppleFlatBuffers.framework/Versions/A/AppleFlatBuffers +0x000000026eb72000 /System/Library/PrivateFrameworks/SentencePieceInternal.framework/Versions/A/SentencePieceInternal +0x000000019f77a000 /System/Library/Frameworks/Vision.framework/Versions/A/Vision +0x0000000246298000 /System/Library/PrivateFrameworks/CoreSceneUnderstanding.framework/Versions/A/CoreSceneUnderstanding +0x00000002811df000 /System/Library/PrivateFrameworks/VisionCore.framework/Versions/A/VisionCore +0x00000001999f0000 /System/Library/PrivateFrameworks/DataDetectorsCore.framework/Versions/A/DataDetectorsCore +0x00000001bdbc1000 /System/Library/Frameworks/Vision.framework/libfaceCore.dylib +0x00000001be6db000 /System/Library/PrivateFrameworks/Futhark.framework/Versions/A/Futhark +0x00000001c2629000 /System/Library/PrivateFrameworks/InertiaCam.framework/Versions/A/InertiaCam +0x00000001be468000 /System/Library/PrivateFrameworks/TextRecognition.framework/Versions/A/TextRecognition +0x000000022eadd000 /System/Library/Frameworks/DataDetection.framework/Versions/A/DataDetection +0x00000001b8674000 /System/Library/PrivateFrameworks/TextInput.framework/Versions/A/TextInput +0x000000019849e000 /System/Library/PrivateFrameworks/CVNLP.framework/Versions/A/CVNLP +0x00000001dad09000 /System/Library/PrivateFrameworks/HIDDisplay.framework/Versions/A/HIDDisplay +0x000000019b255000 /usr/lib/libcups.2.dylib +0x000000019b2ec000 /System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos +0x000000019af5c000 /usr/lib/libresolv.9.dylib +0x0000000198958000 /System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal +0x00000001a4100000 /System/Library/Frameworks/Kerberos.framework/Versions/A/Libraries/libHeimdalProxy.dylib +0x000000019b350000 /System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth +0x00000001ad40e000 /System/Library/PrivateFrameworks/AXCoreUtilities.framework/Versions/A/AXCoreUtilities +0x00000001bda0a000 /System/Library/PrivateFrameworks/AttributeGraph.framework/Versions/A/AttributeGraph +0x000000028a801000 /usr/lib/libAXSafeCategoryBundle.dylib +0x0000000235252000 /System/Library/Frameworks/TabularData.framework/Versions/A/TabularData +0x000000023c11d000 /System/Library/PrivateFrameworks/ArgumentParserInternal.framework/Versions/A/ArgumentParserInternal +0x0000000195a5e000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib +0x0000000197053000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib +0x0000000195f95000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib +0x0000000196ec7000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib +0x000000019704e000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib +0x00000001949d5000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib +0x0000000188221000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib +0x000000022e5c2000 /System/Library/Frameworks/CoreTransferable.framework/Versions/A/CoreTransferable +0x000000019b2b4000 /System/Library/PrivateFrameworks/NetAuth.framework/Versions/A/NetAuth +0x00000001918b4000 /System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport +0x000000018ca51000 /System/Library/PrivateFrameworks/UIFoundation.framework/Versions/A/UIFoundation +0x0000000195efd000 /usr/lib/libCheckFix.dylib +0x0000000190a3b000 /System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities +0x00000002569c7000 /System/Library/PrivateFrameworks/InstalledContentLibrary.framework/Versions/A/InstalledContentLibrary +0x000000018b192000 /System/Library/PrivateFrameworks/CoreServicesStore.framework/Versions/A/CoreServicesStore +0x00000001916ff000 /usr/lib/libapp_launch_measurement.dylib +0x00000001c8192000 /System/Library/PrivateFrameworks/MobileSystemServices.framework/Versions/A/MobileSystemServices +0x0000000198323000 /usr/lib/libxslt.1.dylib +0x0000000195ebc000 /System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement +0x00000001a3792000 /usr/lib/libcurl.4.dylib +0x000000028b517000 /usr/lib/libcrypto.46.dylib +0x000000028c09a000 /usr/lib/libssl.48.dylib +0x00000001a346c000 /System/Library/Frameworks/LDAP.framework/Versions/A/LDAP +0x00000001a34a8000 /System/Library/PrivateFrameworks/TrustEvaluationAgent.framework/Versions/A/TrustEvaluationAgent +0x000000019af79000 /usr/lib/libsasl2.2.dylib +0x00000001a6710000 /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa +0x000000018b32d000 /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit +0x000000023ffce000 /System/Library/PrivateFrameworks/CollectionViewCore.framework/Versions/A/CollectionViewCore +0x0000000193f6f000 /System/Library/PrivateFrameworks/XCTTargetBootstrap.framework/Versions/A/XCTTargetBootstrap +0x0000000199a42000 /System/Library/PrivateFrameworks/UserActivity.framework/Versions/A/UserActivity +0x0000000249ab0000 /System/Library/PrivateFrameworks/FrontBoard.framework/Versions/A/FrontBoard +0x000000027df9f000 /System/Library/PrivateFrameworks/UIIntelligenceSupport.framework/Versions/A/UIIntelligenceSupport +0x00000002342db000 /System/Library/Frameworks/SwiftUICore.framework/Versions/A/SwiftUICore +0x0000000284b4b000 /System/Library/PrivateFrameworks/WritingTools.framework/Versions/A/WritingTools +0x0000000283942000 /System/Library/PrivateFrameworks/WindowManagement.framework/Versions/A/WindowManagement +0x00000002497e0000 /System/Library/PrivateFrameworks/FocusEngine.framework/Versions/A/FocusEngine +0x00000002471e9000 /System/Library/PrivateFrameworks/DesignLibrary.framework/Versions/A/DesignLibrary +0x0000000193f5a000 /System/Library/PrivateFrameworks/DFRFoundation.framework/Versions/A/DFRFoundation +0x000000027eeec000 /System/Library/PrivateFrameworks/UpdateCycle.framework/Versions/A/UpdateCycle +0x0000000193c5e000 /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox +0x000000019f040000 /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/SpeechRecognition.framework/Versions/A/SpeechRecognition +0x0000000191686000 /System/Library/PrivateFrameworks/PerformanceAnalysis.framework/Versions/A/PerformanceAnalysis +0x000000019f3d0000 /System/Library/Frameworks/Accessibility.framework/Versions/A/Accessibility +0x0000000235238000 /System/Library/Frameworks/Symbols.framework/Versions/A/Symbols +0x0000000252dac000 /System/Library/PrivateFrameworks/Gestures.framework/Versions/A/Gestures +0x000000028c4c2000 /usr/lib/swift/libswiftSpatial.dylib +0x00000001b164e000 /usr/lib/swift/libswiftCoreGraphics.dylib +0x000000019fe14000 /usr/lib/swift/libswiftFoundation.dylib +0x00000001ebe32000 /usr/lib/swift/libswiftSwiftOnoneSupport.dylib +0x000000028c748000 /usr/lib/swift/libswiftsys_time.dylib +0x00000001d699f000 /System/Library/PrivateFrameworks/CoreMaterial.framework/Versions/A/CoreMaterial +0x000000028acfe000 /usr/lib/libSpatial.dylib +0x000000028a71e000 /System/Library/SubFrameworks/UIUtilities.framework/Versions/A/UIUtilities +0x0000000102004000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/server/libjvm.dylib +0x0000000100db8000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjimage.dylib +0x0000000100e14000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjdwp.dylib +0x0000000100e5c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjava.dylib +0x0000000100de8000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libinstrument.dylib +0x0000000100ed0000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +0x000000028a7f3000 /usr/lib/i18n/libiconv_std.dylib +0x000000028a7e9000 /usr/lib/i18n/libUTF8.dylib +0x000000028a7f8000 /usr/lib/i18n/libmapper_none.dylib +0x0000000100f5c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libdt_socket.dylib +0x0000000101058000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnio.dylib +0x000000010109c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnet.dylib +0x0000000101038000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement.dylib +0x0000000101078000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement_ext.dylib +0x00000001010c0000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libextnet.dylib +0x0000000101200000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libverify.dylib + + +VM Arguments: +jvm_args: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:50259,suspend=y,server=n -javaagent:/Users/liangxin/Library/Caches/JetBrains/IntelliJIdea2026.1/captureAgent/debugger-agent.jar=file:///var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/capture10140205674518191061.props -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Ddebugger.async.stack.trace.for.all.threads=true -Dfile.encoding=UTF-8 +java_command: org.springblade.desk.DeskApplication +java_class_path (initial): /Users/liangxin/Project/JAVA/tms-erp-api/blade-service/blade-desk/target/classes:/Users/liangxin/.m2/repository/org/springblade/blade-core-boot/4.10.0.BASE-SNAPSHOT/blade-core-boot-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-context/4.10.0.BASE-SNAPSHOT/blade-core-context-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-db/4.10.0.BASE-SNAPSHOT/blade-core-db-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-starter-jdbc/3.5.16/spring-boot-starter-jdbc-3.5.16.jar:/Users/liangxin/.m2/repository/com/zaxxer/HikariCP/6.3.3/HikariCP-6.3.3.jar:/Users/liangxin/.m2/repository/com/baomidou/mybatis-plus-spring-boot3-starter/3.5.16/mybatis-plus-spring-boot3-starter-3.5.16.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/3.5.16/spring-boot-autoconfigure-3.5.16.jar:/Users/liangxin/.m2/repository/com/alibaba/druid-spring-boot-3-starter/1.2.28/druid-spring-boot-3-starter-1.2.28.jar:/Users/liangxin/.m2/repository/com/mysql/mysql-connector-j/9.4.0/mysql-connector-j-9.4.0.jar:/Users/liangxin/.m2/repository/com/google/protobuf/protobuf-java/4.31.1/protobuf-java-4.31.1.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-secure/4.10.0.BASE-SNAPSHOT/blade-core-secure-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springblade/blade-core-cloud/4.10.0.BASE-SNAPSHOT/blade-core-cloud-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/de/codecentric/spring-boot-admin-starter-client/3.5.9/spring-boot-admin-starter-client-3.5.9.jar:/Users/liangxin/.m2/repository/de/codecentric/spring-boot-admin-client/3.5.9/spring-boot-admin-client-3.5.9.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-starter-actuator/3.5.16/spring-boot-starter-actuator-3.5.16.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-actuator-autoconfigure/3.5.16/spring-boot-actuator-aut +Launcher Type: SUN_STANDARD + +[Global flags] + intx CICompilerCount = 4 {product} {ergonomic} + uint ConcGCThreads = 3 {product} {ergonomic} + uint G1ConcRefinementThreads = 10 {product} {ergonomic} + size_t G1HeapRegionSize = 8388608 {product} {ergonomic} + uintx GCDrainStackTargetSize = 64 {product} {ergonomic} + size_t InitialHeapSize = 603979776 {product} {ergonomic} + bool ManagementServer = true {product} {command line} + size_t MarkStackSize = 4194304 {product} {ergonomic} + size_t MaxHeapSize = 9663676416 {product} {ergonomic} + size_t MaxNewSize = 5796528128 {product} {ergonomic} + size_t MinHeapDeltaBytes = 8388608 {product} {ergonomic} + size_t MinHeapSize = 8388608 {product} {ergonomic} + uintx NonProfiledCodeHeapSize = 0 {pd product} {ergonomic} + bool ProfileInterpreter = false {pd product} {command line} + uintx ProfiledCodeHeapSize = 0 {pd product} {ergonomic} + size_t SoftMaxHeapSize = 9663676416 {manageable} {ergonomic} + intx TieredStopAtLevel = 1 {product} {command line} + bool UseCompressedClassPointers = true {product lp64_product} {ergonomic} + bool UseCompressedOops = true {product lp64_product} {ergonomic} + bool UseG1GC = true {product} {ergonomic} + bool UseNUMA = false {product} {ergonomic} + bool UseNUMAInterleaving = false {product} {ergonomic} + +Logging: +Log output configuration: + #0: stdout all=warning uptime,level,tags + #1: stderr all=off uptime,level,tags + +Environment Variables: +JAVA_HOME=/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home +PATH=/Users/liangxin/ai-infra/.venv/bin:/Users/liangxin/.nacos/bin:/Applications/Docker.app/Contents/Resources/bin:/Users/liangxin/Library/pnpm:/opt/homebrew/opt/ruby@3.2/bin:/opt/homebrew/opt/openssl@3/bin:/opt/miniconda3/bin:/opt/miniconda3/condabin:/usr/local/sbin:/usr/local/bin:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Users/liangxin/fvm/default/bin:/Applications/MAMP/bin/php/php8.1.13/bin:/opt/homebrew/opt/ruby@3.2/bin:/Users/liangxin/.nvm/versions/node/v20.18.3/bin:/Applications/apache-tomcat-9.0.78:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Users/liangxin/fvm/default/bin:/opt/homebrew/opt/libpng/bin:/Applications/pngquant:/Users/liangxin/AndroidSDK/platform-tools:/Users/liangxin/Library/Android/sdk/platform-tools:/Users/liangxin/Library/Andriod/sdk/cmdline-tools/latest/bin:/Users/liangxin/Library/Andriod/sdk:/Applications/apache-maven-3.8.1/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/local/bin:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/Users/liangxin/.local/bin:/Users/liangxin/fvm/default/bin:/Applications/MAMP/bin/php/php8.1.13/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/liangxin/.cargo/bin:true:/Applications/极空间.app/Contents/Resources/app.asar.unpacked/bin/platform-tools +SHELL=/bin/zsh +LANG=C.UTF-8 +TMPDIR=/var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/ + +Active Locale: +LC_ALL=C.UTF-8 +LC_COLLATE=C.UTF-8 +LC_CTYPE=C.UTF-8 +LC_MESSAGES=C.UTF-8 +LC_MONETARY=C.UTF-8 +LC_NUMERIC=C.UTF-8 +LC_TIME=C.UTF-8 + +Signal Handlers: + SIGSEGV: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGBUS: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGFPE: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGPIPE: javaSignalHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGXFSZ: javaSignalHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGILL: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGUSR2: SR_handler in libjvm.dylib, mask=00000000000000000000000000000000, flags=SA_RESTART|SA_SIGINFO, blocked + SIGHUP: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGINT: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGTERM: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGQUIT: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGTRAP: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + + +--------------- S Y S T E M --------------- + +OS: +uname: Darwin 25.6.0 Darwin Kernel Version 25.6.0: Fri Jul 31 19:16:36 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6030 arm64 +OS uptime: 2 days 23:37 hours +rlimit (soft/hard): STACK 8176k/65520k , CORE 0k/infinity , NPROC 6000/9000 , NOFILE 10240/infinity , AS infinity/infinity , CPU infinity/infinity , DATA infinity/infinity , FSIZE infinity/infinity , MEMLOCK infinity/infinity , RSS infinity/infinity +load average: 19.31 16.17 13.60 + +CPU: total 12 (initial active 12) 0x61:0x0:0x5f4dea93:0, fp, simd, crc, lse +machdep.cpu.brand_string:Apple M3 Pro +hw.cachelinesize:128 +hw.l1icachesize:131072 +hw.l1dcachesize:65536 +hw.l2cachesize:4194304 + +Memory: 16k page, physical 37748736k(166608k free), swap 16777216k(965120k free) + +vm_info: OpenJDK 64-Bit Server VM (17.0.8+7-LTS) for bsd-aarch64 JRE (17.0.8+7-LTS) (Zulu17.44+15-CA), built on Jul 5 2023 00:50:04 by "zulu_re" with clang Apple LLVM 12.0.0 (clang-1200.0.32.28) + +END. diff --git a/hs_err_pid40988.log b/hs_err_pid40988.log new file mode 100644 index 0000000..52e4b4e --- /dev/null +++ b/hs_err_pid40988.log @@ -0,0 +1,1368 @@ +# +# A fatal error has been detected by the Java Runtime Environment: +# +# SIGBUS (0xa) at pc=0x00000001046de4c0, pid=40988, tid=36131 +# +# JRE version: OpenJDK Runtime Environment Zulu17.44+15-CA (17.0.8+7) (build 17.0.8+7-LTS) +# Java VM: OpenJDK 64-Bit Server VM Zulu17.44+15-CA (17.0.8+7-LTS, mixed mode, emulated-client, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64) +# Problematic frame: +# C [libzip.dylib+0x64c0] newEntry+0x68 +# +# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again +# +# If you would like to submit a bug report, please visit: +# http://www.azul.com/support/ +# The crash happened outside the Java Virtual Machine in native code. +# See problematic frame for where to report the bug. +# + +--------------- S U M M A R Y ------------ + +Command Line: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:49167,suspend=y,server=n -javaagent:/Users/liangxin/Library/Caches/JetBrains/IntelliJIdea2026.1/captureAgent/debugger-agent.jar=file:///var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/capture18007844485071508468.props -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Ddebugger.async.stack.trace.for.all.threads=true -Dfile.encoding=UTF-8 org.springblade.resource.ResourceApplication + +Host: "Mac15,6" arm64, 12 cores, 36G, Darwin 25.6.0, macOS 26.6.2 (25G83) +Time: Thu Sep 17 20:22:18 2026 CST elapsed time: 7.706668 seconds (0d 0h 0m 7s) + +--------------- T H R E A D --------------- + +Current thread (0x000000010fa93c00): JavaThread "sentinel-datafile-log-executor-thread-1" daemon [_thread_in_native, id=36131, stack(0x0000000318210000,0x0000000318413000)] + +Stack: [0x0000000318210000,0x0000000318413000], sp=0x00000003184119d0, free space=2054k +Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) +C [libzip.dylib+0x64c0] newEntry+0x68 +C [libzip.dylib+0x6390] ZIP_GetEntry2+0x14c +C [libzip.dylib+0x6d78] ZIP_FindEntry+0x3c +V [libjvm.dylib+0x25a408] ClassPathZipEntry::open_entry(JavaThread*, char const*, int*, bool)+0xb4 +V [libjvm.dylib+0x25a53c] ClassPathZipEntry::open_stream(JavaThread*, char const*)+0x20 +V [libjvm.dylib+0x25d918] ClassLoader::load_class(Symbol*, bool, JavaThread*)+0x150 +V [libjvm.dylib+0x981d60] SystemDictionary::load_instance_class_impl(Symbol*, Handle, JavaThread*)+0x2d0 +V [libjvm.dylib+0x98063c] SystemDictionary::load_instance_class(unsigned int, Symbol*, Handle, JavaThread*)+0x30 +V [libjvm.dylib+0x97fd48] SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, JavaThread*)+0x4dc +V [libjvm.dylib+0x97f334] SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, JavaThread*)+0x80 +V [libjvm.dylib+0x2beb54] ConstantPool::klass_at_impl(constantPoolHandle const&, int, JavaThread*)+0x1e0 +V [libjvm.dylib+0x46d6f0] InterpreterRuntime::_new(JavaThread*, ConstantPool*, int)+0x94 +j com.intellij.rt.debugger.agent.CaptureStorage$DeepCapturedStack.collectStacks(Ljava/util/List;)Lcom/intellij/rt/debugger/agent/CaptureStorage$StackData;+89 +j com.intellij.rt.debugger.agent.CaptureStorage.getStackTrace(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+30 +j com.intellij.rt.debugger.agent.CaptureStorage.access$1000(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()[Ljava/lang/StackTraceElement;+31 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()Ljava/lang/Object;+1 +j com.intellij.rt.debugger.agent.CaptureStorage.withoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$Callable;)Ljava/lang/Object;+21 +j com.intellij.rt.debugger.agent.CaptureStorage.getAsyncStackTrace(Ljava/lang/Throwable;)[Ljava/lang/StackTraceElement;+19 +j java.lang.Throwable.printStackTrace(Ljava/lang/Throwable$PrintStreamOrWriter;)V+32 java.base@17.0.8 +j java.lang.Throwable.printStackTrace(Ljava/io/PrintWriter;)V+9 java.base@17.0.8 +j com.alibaba.csp.sentinel.log.jul.CspFormatter.format(Ljava/util/logging/LogRecord;)Ljava/lang/String;+103 +j java.util.logging.StreamHandler.publish(Ljava/util/logging/LogRecord;)V+14 java.logging@17.0.8 +j java.util.logging.FileHandler.publish(Ljava/util/logging/LogRecord;)V+11 java.logging@17.0.8 +j com.alibaba.csp.sentinel.log.jul.DateFileLogHandler$LogTask.run()V+8 +j java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+92 java.base@17.0.8 +j java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5 java.base@17.0.8 +j java.lang.Thread.run()V+11 java.base@17.0.8 +v ~StubRoutines::call_stub +V [libjvm.dylib+0x4781f8] JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)+0x394 +V [libjvm.dylib+0x47720c] JavaCalls::call_virtual(JavaValue*, Klass*, Symbol*, Symbol*, JavaCallArguments*, JavaThread*)+0x11c +V [libjvm.dylib+0x4772d8] JavaCalls::call_virtual(JavaValue*, Handle, Klass*, Symbol*, Symbol*, JavaThread*)+0x64 +V [libjvm.dylib+0x52ebfc] thread_entry(JavaThread*, JavaThread*)+0xc4 +V [libjvm.dylib+0x9b22e8] JavaThread::thread_main_inner()+0x150 +V [libjvm.dylib+0x9b0990] Thread::call_run()+0xe0 +V [libjvm.dylib+0x7d0364] thread_native_entry(Thread*)+0x158 +C [libsystem_pthread.dylib+0x6c58] _pthread_start+0x88 + +Java frames: (J=compiled Java code, j=interpreted, Vv=VM code) +j com.intellij.rt.debugger.agent.CaptureStorage$DeepCapturedStack.collectStacks(Ljava/util/List;)Lcom/intellij/rt/debugger/agent/CaptureStorage$StackData;+89 +j com.intellij.rt.debugger.agent.CaptureStorage.getStackTrace(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+30 +j com.intellij.rt.debugger.agent.CaptureStorage.access$1000(Lcom/intellij/rt/debugger/agent/CaptureStorage$CapturedStack;I)Ljava/util/ArrayList;+2 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()[Ljava/lang/StackTraceElement;+31 +j com.intellij.rt.debugger.agent.CaptureStorage$9.call()Ljava/lang/Object;+1 +j com.intellij.rt.debugger.agent.CaptureStorage.withoutThrowableCapture(Lcom/intellij/rt/debugger/agent/CaptureStorage$Callable;)Ljava/lang/Object;+21 +j com.intellij.rt.debugger.agent.CaptureStorage.getAsyncStackTrace(Ljava/lang/Throwable;)[Ljava/lang/StackTraceElement;+19 +j java.lang.Throwable.printStackTrace(Ljava/lang/Throwable$PrintStreamOrWriter;)V+32 java.base@17.0.8 +j java.lang.Throwable.printStackTrace(Ljava/io/PrintWriter;)V+9 java.base@17.0.8 +j com.alibaba.csp.sentinel.log.jul.CspFormatter.format(Ljava/util/logging/LogRecord;)Ljava/lang/String;+103 +j java.util.logging.StreamHandler.publish(Ljava/util/logging/LogRecord;)V+14 java.logging@17.0.8 +j java.util.logging.FileHandler.publish(Ljava/util/logging/LogRecord;)V+11 java.logging@17.0.8 +j com.alibaba.csp.sentinel.log.jul.DateFileLogHandler$LogTask.run()V+8 +j java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+92 java.base@17.0.8 +j java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5 java.base@17.0.8 +j java.lang.Thread.run()V+11 java.base@17.0.8 +v ~StubRoutines::call_stub + +siginfo: si_signo: 10 (SIGBUS), si_code: 1 (BUS_ADRALN), si_addr: 0x00000001046c5e7b + +Register to memory mapping: + + x0=0x0000600000694eb0 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x1=0x0 is NULL + x2=0x0 is NULL + x3=0x0000600000694ec0 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 + x4=0x0000600000694f00 points into unknown readable memory: 0x0000600003c95440 | 40 54 c9 03 00 60 00 00 + x5=0x000000009d64e030 is an unknown value + x6=0x0000000000000eb0 is an unknown value + x7=0x000000000000000a is an unknown value + x8=0x00000001047ede5f: gdata+0xcbf7 in /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjdwp.dylib at 0x00000001047a8000 + x9=0x0000000000128000 is an unknown value +x10=0x0000600000694eb0 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x11=0x000066d8c3c50000 is an unknown value +x12=0x0000000000000050 is an unknown value +x13=0x000060000242e518 points into unknown readable memory: 0x000000009d84d82f | 2f d8 84 9d 00 00 00 00 +x14=0x00000000001ff800 is an unknown value +x15=0x00000000000007fb is an unknown value +x16=0x0000000186e7d030: __bzero+0 in /usr/lib/system/libsystem_platform.dylib at 0x0000000186e7a000 +x17=0x00000001f4ef54a8 points into unknown readable memory: 0x0000000186e7d030 | 30 d0 e7 86 01 00 00 00 +x18=0x0 is NULL +x19=0x0000600000694eb0 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x20=0x0 is NULL +x21=0x0000600001324000 points into unknown readable memory: 0x00006000002382a0 | a0 82 23 00 00 60 00 00 +x22=0x00000001046c5e5f points into unknown readable memory: 50 +x23=0x00000000d3a18b02 is an unknown value +x24=0x000000000000002f is an unknown value +x25=0x000000000000003d is an unknown value +x26=0x00000000000000cd is an unknown value +x27=0x0000600000694ed8 points into unknown readable memory: 0x0000000000000000 | 00 00 00 00 00 00 00 00 +x28=0x0000000105127d40 points into unknown readable memory: 0x65746e692f6d6f63 | 63 6f 6d 2f 69 6e 74 65 + + +Registers: + x0=0x0000600000694eb0 x1=0x0000000000000000 x2=0x0000000000000000 x3=0x0000600000694ec0 + x4=0x0000600000694f00 x5=0x000000009d64e030 x6=0x0000000000000eb0 x7=0x000000000000000a + x8=0x00000001047ede5f x9=0x0000000000128000 x10=0x0000600000694eb0 x11=0x000066d8c3c50000 +x12=0x0000000000000050 x13=0x000060000242e518 x14=0x00000000001ff800 x15=0x00000000000007fb +x16=0x0000000186e7d030 x17=0x00000001f4ef54a8 x18=0x0000000000000000 x19=0x0000600000694eb0 +x20=0x0000000000000000 x21=0x0000600001324000 x22=0x00000001046c5e5f x23=0x00000000d3a18b02 +x24=0x000000000000002f x25=0x000000000000003d x26=0x00000000000000cd x27=0x0000600000694ed8 +x28=0x0000000105127d40 fp=0x0000000318411a50 lr=0x00000001046de48c sp=0x00000003184119d0 +pc=0x00000001046de4c0 cpsr=0x0000000060001000 +Top of Stack: (sp=0x00000003184119d0) +0x00000003184119d0: 0000000000000000 0000000000000000 +0x00000003184119e0: 0000000000000000 0000000000000000 +0x00000003184119f0: 0000000000000000 0000000000000000 +0x0000000318411a00: 0000000105127d40 0000000107809600 +0x0000000318411a10: 00000000000000cd 000000000000003d +0x0000000318411a20: 000000000000002f 00000000d3a18b02 +0x0000000318411a30: 0000600000694f00 0000000000000000 +0x0000000318411a40: 0000000105127d80 0000600001324000 +0x0000000318411a50: 0000000318411ab0 00000001046de390 +0x0000000318411a60: 0000000105127d40 0000000000000037 +0x0000000318411a70: 0000000000000001 0000000105127d80 +0x0000000318411a80: 000000010fa93f48 0000000105127d80 +0x0000000318411a90: 0000600001324000 0000000105127d80 +0x0000000318411aa0: 0000000318411bdc 0000000318411af4 +0x0000000318411ab0: 0000000318411ae0 00000001046ded78 +0x0000000318411ac0: 0000000318411bdc 00006000028341e0 +0x0000000318411ad0: 0000000000000000 000000010fa93c00 +0x0000000318411ae0: 0000000318411bc0 0000000105a1a408 +0x0000000318411af0: 0000000106278388 0000000000000100 +0x0000000318411b00: 0000000318411b20 0000000105cee5dc +0x0000000318411b10: 0000000106278388 0000000318411ba0 +0x0000000318411b20: 0000000318411b70 0000000105aa096c +0x0000000318411b30: 0000000000000000 0000000000000000 +0x0000000318411b40: 0000000000000001 0000000158a50290 +0x0000000318411b50: 000000010fa93c00 0000000105128118 +0x0000000318411b60: 000000010628d1e2 0000000318411c68 +0x0000000318411b70: 0000000318411b90 43eef1f7fdfa0026 +0x0000000318411b80: 0000000000000001 0000000105127d80 +0x0000000318411b90: 00006000028341e0 0000000158a50290 +0x0000000318411ba0: 000000010fa93c00 0000000105128118 +0x0000000318411bb0: 0000000105127d30 00006000028341e0 +0x0000000318411bc0: 0000000318411bf0 0000000105a1a53c + +Instructions: (pc=0x00000001046de4c0) +0x00000001046de3c0: 6b0c017f 54ffff60 17ffffde d2800016 +0x00000001046de3d0: 72001ebf 54000160 b5000156 f100073f +0x00000001046de3e0: 54fff7cb 8b140328 385ff108 7100bd1f +0x00000001046de3f0: 54fff741 d2800016 14000002 f9004e7f +0x00000001046de400: f9402a60 94000451 aa1603e0 a9457bfd +0x00000001046de410: a9444ff4 a94357f6 a9425ff8 a94167fa +0x00000001046de420: a8c66ffc d65f03c0 6b03003f 540000e1 +0x00000001046de430: 71000421 540000eb 38401408 38401449 +0x00000001046de440: 6b09011f 54ffff60 52800000 d65f03c0 +0x00000001046de450: 52800020 d65f03c0 d10243ff a9036ffc +0x00000001046de460: a90467fa a9055ff8 a90657f6 a9074ff4 +0x00000001046de470: a9087bfd 910203fd aa0203f4 aa0103f6 +0x00000001046de480: aa0003f5 52800900 94000481 aa0003f3 +0x00000001046de490: b4001320 f900027f aa1303fb f8028f7f +0x00000001046de4a0: f9001a7f 3940c2a8 34000288 f9400ea8 +0x00000001046de4b0: f94006c9 8b090108 f94016a9 cb090116 +0x00000001046de4c0: 79403ad8 39407ada 39407edc 794042c8 +0x00000001046de4d0: f90017e8 b9400ec8 f9000668 b9401ac8 +0x00000001046de4e0: f9000fe8 f9000a68 794016c8 34000488 +0x00000001046de4f0: b94016c8 14000023 f94006d7 34000d54 +0x00000001046de500: f9401ea8 b4000288 f94022a9 eb17013f +0x00000001046de510: 5400022c 5283fa4a 8b0a012a eb17015f +0x00000001046de520: 540001ab 9140092a 8b170108 cb090116 +0x00000001046de530: 79403ac8 79403ec9 794042cb 8b0802e8 +0x00000001046de540: 8b090108 8b0b0108 9100b908 eb0a011f +0x00000001046de550: 54000b4d aa1503e0 aa1703e1 52840002 +0x00000001046de560: 94000384 aa0003f6 b4000aa0 f9401ea0 +0x00000001046de570: 94000429 a903deb6 17ffffd2 d2800008 +0x00000001046de580: aa0803f7 f9000e68 b94012c8 b9002268 +0x00000001046de590: b842a2c9 f9405ea8 f9000be9 8b090108 +0x00000001046de5a0: cb0803e8 f9001e68 794012c8 b9004268 +0x00000001046de5b0: 91000700 94000436 aa0003f9 f9000260 + + +Stack slot to memory mapping: +stack at sp + 0 slots: 0x0 is NULL +stack at sp + 1 slots: 0x0 is NULL +stack at sp + 2 slots: 0x0 is NULL +stack at sp + 3 slots: 0x0 is NULL +stack at sp + 4 slots: 0x0 is NULL +stack at sp + 5 slots: 0x0 is NULL +stack at sp + 6 slots: 0x0000000105127d40 points into unknown readable memory: 0x65746e692f6d6f63 | 63 6f 6d 2f 69 6e 74 65 +stack at sp + 7 slots: 0x0000000107809600 points into unknown readable memory: 0xffffffff5bbd78a2 | a2 78 bd 5b ff ff ff ff + + +--------------- P R O C E S S --------------- + +Threads class SMR info: +_java_thread_list=0x000060000217cce0, length=71, elements={ +0x0000000107808a00, 0x000000011b808200, 0x000000010f813a00, 0x0000000107009200, +0x0000000107009800, 0x000000011e014a00, 0x000000011b00c400, 0x0000000107823800, +0x000000011c00a200, 0x000000011a813c00, 0x000000011a816400, 0x000000011e80fa00, +0x000000011a0d9600, 0x000000010f812c00, 0x000000011e02fe00, 0x000000011b1b6800, +0x000000011a573600, 0x000000011c8e8800, 0x000000011b960000, 0x0000000107024a00, +0x000000011f3d8600, 0x000000011eba7600, 0x000000010f9e0600, 0x000000011f41b600, +0x000000011c068400, 0x000000011f47fc00, 0x000000011c047c00, 0x000000011f440200, +0x000000011b404800, 0x000000011f4a3a00, 0x000000011c07bc00, 0x000000011c095c00, +0x000000011b95a800, 0x000000011e499200, 0x000000011e4bd000, 0x000000011b4a8800, +0x000000011a66aa00, 0x000000011f504600, 0x000000011ad0cc00, 0x000000011f598a00, +0x000000011b9bb000, 0x000000011a693400, 0x000000011f3e9600, 0x0000000107978800, +0x0000000107973c00, 0x0000000107934000, 0x0000000107961000, 0x000000011b989600, +0x000000011ad8e400, 0x000000011ad93000, 0x000000011e4a8000, 0x000000011b52f200, +0x000000011b9a0a00, 0x000000011a69d400, 0x000000011b563c00, 0x000000010fa93c00, +0x000000011b5a3a00, 0x000000011e51f600, 0x000000011f65ba00, 0x000000011b62e600, +0x000000011edd6200, 0x000000011edf8e00, 0x000000011f67a800, 0x000000011ae25600, +0x000000011ae41800, 0x000000011ae41e00, 0x000000011edf4400, 0x000000011edef000, +0x000000011b3b4400, 0x0000000107ada400, 0x000000011b77a400 +} + +Java Threads: ( => current thread ) + 0x0000000107808a00 JavaThread "main" [_thread_in_native, id=4355, stack(0x000000016b888000,0x000000016ba8b000)] + 0x000000011b808200 JavaThread "Reference Handler" daemon [_thread_blocked, id=18179, stack(0x000000016c6dc000,0x000000016c8df000)] + 0x000000010f813a00 JavaThread "Finalizer" daemon [_thread_blocked, id=18947, stack(0x000000016c8e8000,0x000000016caeb000)] + 0x0000000107009200 JavaThread "Signal Dispatcher" daemon [_thread_blocked, id=31235, stack(0x000000016cc0c000,0x000000016ce0f000)] + 0x0000000107009800 JavaThread "Service Thread" daemon [_thread_blocked, id=30723, stack(0x000000016ce18000,0x000000016d01b000)] + 0x000000011e014a00 JavaThread "Monitor Deflation Thread" daemon [_thread_blocked, id=23299, stack(0x000000016d024000,0x000000016d227000)] + 0x000000011b00c400 JavaThread "C1 CompilerThread0" daemon [_thread_blocked, id=30211, stack(0x000000016d230000,0x000000016d433000)] + 0x0000000107823800 JavaThread "Sweeper thread" daemon [_thread_blocked, id=24067, stack(0x000000016d43c000,0x000000016d63f000)] + 0x000000011c00a200 JavaThread "Common-Cleaner" daemon [_thread_blocked, id=24579, stack(0x000000016d648000,0x000000016d84b000)] + 0x000000011a813c00 JavaThread "JDWP Transport Listener: dt_socket" daemon [_thread_blocked, id=25091, stack(0x000000016d854000,0x000000016da57000)] + 0x000000011a816400 JavaThread "JDWP Event Helper Thread" daemon [_thread_blocked, id=29699, stack(0x000000016da60000,0x000000016dc63000)] + 0x000000011e80fa00 JavaThread "JDWP Command Reader" daemon [_thread_in_native, id=29443, stack(0x000000016dc6c000,0x000000016de6f000)] + 0x000000011a0d9600 JavaThread "IntelliJ Suspend Helper" daemon [_thread_blocked, id=28931, stack(0x000000016de78000,0x000000016e07b000)] + 0x000000010f812c00 JavaThread "Notification Thread" daemon [_thread_blocked, id=25603, stack(0x000000016e084000,0x000000016e287000)] + 0x000000011e02fe00 JavaThread "CoarseTimer" daemon [_thread_blocked, id=28163, stack(0x000000016e290000,0x000000016e493000)] + 0x000000011b1b6800 JavaThread "RMI TCP Accept-0" daemon [_thread_in_native, id=34307, stack(0x000000016f708000,0x000000016f90b000)] + 0x000000011a573600 JavaThread "com.alibaba.nacos.client.logging.0" daemon [_thread_blocked, id=35331, stack(0x000000016fb20000,0x000000016fd23000)] + 0x000000011c8e8800 JavaThread "Attach Listener" daemon [_thread_blocked, id=42243, stack(0x000000016fd2c000,0x000000016ff2f000)] + 0x000000011b960000 JavaThread "RMI TCP Connection(1)-127.0.0.1" daemon [_thread_in_native, id=40963, stack(0x0000000318a40000,0x0000000318c43000)] + 0x0000000107024a00 JavaThread "nacos.publisher-com.alibaba.nacos.common.notify.SlowEvent" daemon [_thread_blocked, id=37635, stack(0x0000000318c4c000,0x0000000318e4f000)] + 0x000000011f3d8600 JavaThread "nacos.publisher-com.alibaba.nacos.client.config.impl.ConfigFuzzyWatchNotifyEvent" daemon [_thread_blocked, id=40451, stack(0x0000000318e58000,0x000000031905b000)] + 0x000000011eba7600 JavaThread "nacos.publisher-com.alibaba.nacos.client.config.impl.ConfigFuzzyWatchLoadEvent" daemon [_thread_blocked, id=39939, stack(0x0000000319064000,0x0000000319267000)] + 0x000000010f9e0600 JavaThread "RMI Scheduler(0)" daemon [_thread_blocked, id=38915, stack(0x0000000319688000,0x000000031988b000)] + 0x000000011f41b600 JavaThread "com.alibaba.nacos.client.auth.ram.identify.watcher.0" daemon [_thread_blocked, id=43527, stack(0x0000000319894000,0x0000000319a97000)] + 0x000000011c068400 JavaThread "com.alibaba.nacos.client.login-executor.0" daemon [_thread_blocked, id=37127, stack(0x0000000318834000,0x0000000318a37000)] + 0x000000011f47fc00 JavaThread "com.alibaba.nacos.client.listen-executor.0" daemon [_thread_blocked, id=43779, stack(0x0000000319aa0000,0x0000000319ca3000)] + 0x000000011c047c00 JavaThread "com.alibaba.nacos.client.fuzzy-watcher-executor.0" daemon [_thread_blocked, id=44291, stack(0x0000000319cac000,0x0000000319eaf000)] + 0x000000011f440200 JavaThread "com.alibaba.nacos.client.remote.worker.0" daemon [_thread_blocked, id=44803, stack(0x0000000319eb8000,0x000000031a0bb000)] + 0x000000011b404800 JavaThread "com.alibaba.nacos.client.remote.worker.1" daemon [_thread_blocked, id=64771, stack(0x000000031a0c4000,0x000000031a2c7000)] + 0x000000011f4a3a00 JavaThread "grpc-nio-worker-ELG-1-1" daemon [_thread_in_native, id=64531, stack(0x000000031a2d0000,0x000000031a4d3000)] + 0x000000011c07bc00 JavaThread "grpc-default-executor-0" daemon [_thread_blocked, id=64003, stack(0x000000031a4dc000,0x000000031a6df000)] + 0x000000011c095c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-0" daemon [_thread_blocked, id=45315, stack(0x000000031a6e8000,0x000000031a8eb000)] + 0x000000011b95a800 JavaThread "nacos-grpc-client-executor-127.0.0.1-1" daemon [_thread_blocked, id=45827, stack(0x000000031a8f4000,0x000000031aaf7000)] + 0x000000011e499200 JavaThread "grpc-nio-worker-ELG-1-2" daemon [_thread_in_native, id=46083, stack(0x000000031ab00000,0x000000031ad03000)] + 0x000000011e4bd000 JavaThread "nacos-grpc-client-executor-127.0.0.1-2" daemon [_thread_blocked, id=46339, stack(0x000000031ad0c000,0x000000031af0f000)] + 0x000000011b4a8800 JavaThread "nacos-grpc-client-executor-127.0.0.1-3" daemon [_thread_blocked, id=62979, stack(0x000000031af18000,0x000000031b11b000)] + 0x000000011a66aa00 JavaThread "nacos-grpc-client-executor-127.0.0.1-4" daemon [_thread_blocked, id=62467, stack(0x000000031b124000,0x000000031b327000)] + 0x000000011f504600 JavaThread "nacos-grpc-client-executor-127.0.0.1-5" daemon [_thread_blocked, id=46851, stack(0x000000031b330000,0x000000031b533000)] + 0x000000011ad0cc00 JavaThread "nacos-grpc-client-executor-127.0.0.1-6" daemon [_thread_blocked, id=47363, stack(0x000000031b53c000,0x000000031b73f000)] + 0x000000011f598a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-7" daemon [_thread_blocked, id=61955, stack(0x000000031b748000,0x000000031b94b000)] + 0x000000011b9bb000 JavaThread "nacos.publisher-com.alibaba.nacos.common.ability.AbstractAbilityControlManager$AbilityUpdateEvent" daemon [_thread_blocked, id=61443, stack(0x000000031b954000,0x000000031bb57000)] + 0x000000011a693400 JavaThread "nacos-grpc-client-executor-127.0.0.1-8" daemon [_thread_blocked, id=47875, stack(0x000000031bb60000,0x000000031bd63000)] + 0x000000011f3e9600 JavaThread "nacos-grpc-client-executor-127.0.0.1-9" daemon [_thread_blocked, id=60931, stack(0x000000031bd6c000,0x000000031bf6f000)] + 0x0000000107978800 JavaThread "nacos-grpc-client-executor-127.0.0.1-10" daemon [_thread_blocked, id=48387, stack(0x000000031bf78000,0x000000031c17b000)] + 0x0000000107973c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-11" daemon [_thread_blocked, id=48643, stack(0x000000031c184000,0x000000031c387000)] + 0x0000000107934000 JavaThread "nacos-grpc-client-executor-127.0.0.1-12" daemon [_thread_blocked, id=48899, stack(0x000000031c390000,0x000000031c593000)] + 0x0000000107961000 JavaThread "nacos-grpc-client-executor-127.0.0.1-13" daemon [_thread_blocked, id=49155, stack(0x000000031c59c000,0x000000031c79f000)] + 0x000000011b989600 JavaThread "nacos-grpc-client-executor-127.0.0.1-14" daemon [_thread_blocked, id=59395, stack(0x000000031c7a8000,0x000000031c9ab000)] + 0x000000011ad8e400 JavaThread "nacos-grpc-client-executor-127.0.0.1-15" daemon [_thread_blocked, id=49667, stack(0x000000031c9b4000,0x000000031cbb7000)] + 0x000000011ad93000 JavaThread "nacos-grpc-client-executor-127.0.0.1-16" daemon [_thread_blocked, id=58883, stack(0x000000031cbc0000,0x000000031cdc3000)] + 0x000000011e4a8000 JavaThread "nacos-grpc-client-executor-127.0.0.1-17" daemon [_thread_blocked, id=58627, stack(0x000000031cdcc000,0x000000031cfcf000)] + 0x000000011b52f200 JavaThread "nacos-grpc-client-executor-127.0.0.1-18" daemon [_thread_blocked, id=50435, stack(0x000000031cfd8000,0x000000031d1db000)] + 0x000000011b9a0a00 JavaThread "nacos-grpc-client-executor-127.0.0.1-19" daemon [_thread_blocked, id=50955, stack(0x000000031d1e4000,0x000000031d3e7000)] + 0x000000011a69d400 JavaThread "nacos-grpc-client-executor-127.0.0.1-20" daemon [_thread_blocked, id=51203, stack(0x000000031d3f0000,0x000000031d5f3000)] + 0x000000011b563c00 JavaThread "nacos-grpc-client-executor-127.0.0.1-21" daemon [_thread_blocked, id=51459, stack(0x000000031d5fc000,0x000000031d7ff000)] +=>0x000000010fa93c00 JavaThread "sentinel-datafile-log-executor-thread-1" daemon [_thread_in_native, id=36131, stack(0x0000000318210000,0x0000000318413000)] + 0x000000011b5a3a00 JavaThread "sentinel-command-center-executor-thread-1" daemon [_thread_in_native, id=36635, stack(0x0000000318628000,0x000000031882b000)] + 0x000000011e51f600 JavaThread "sentinel-heartbeat-send-task-thread-1" daemon [_thread_blocked, id=41523, stack(0x000000031841c000,0x000000031861f000)] + 0x000000011f65ba00 JavaThread "C1 CompilerThread1" daemon [_thread_blocked, id=36403, stack(0x0000000318004000,0x0000000318207000)] + 0x000000011b62e600 JavaThread "nacos-grpc-client-executor-127.0.0.1-22" daemon [_thread_blocked, id=57635, stack(0x000000031d808000,0x000000031da0b000)] + 0x000000011edd6200 JavaThread "nacos-grpc-client-executor-127.0.0.1-23" daemon [_thread_blocked, id=57095, stack(0x000000031da14000,0x000000031dc17000)] + 0x000000011edf8e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-24" daemon [_thread_blocked, id=56579, stack(0x000000031dc20000,0x000000031de23000)] + 0x000000011f67a800 JavaThread "nacos-grpc-client-executor-127.0.0.1-25" daemon [_thread_blocked, id=51715, stack(0x000000031de2c000,0x000000031e02f000)] + 0x000000011ae25600 JavaThread "nacos-grpc-client-executor-127.0.0.1-26" daemon [_thread_blocked, id=56067, stack(0x000000031e038000,0x000000031e23b000)] + 0x000000011ae41800 JavaThread "nacos-grpc-client-executor-127.0.0.1-27" daemon [_thread_blocked, id=55555, stack(0x000000031e244000,0x000000031e447000)] + 0x000000011ae41e00 JavaThread "nacos-grpc-client-executor-127.0.0.1-28" daemon [_thread_blocked, id=52483, stack(0x000000031e450000,0x000000031e653000)] + 0x000000011edf4400 JavaThread "nacos-grpc-client-executor-127.0.0.1-29" daemon [_thread_blocked, id=55043, stack(0x000000031e65c000,0x000000031e85f000)] + 0x000000011edef000 JavaThread "nacos-grpc-client-executor-127.0.0.1-30" daemon [_thread_blocked, id=54787, stack(0x000000031e868000,0x000000031ea6b000)] + 0x000000011b3b4400 JavaThread "nacos-grpc-client-executor-127.0.0.1-31" daemon [_thread_blocked, id=54531, stack(0x000000031ea74000,0x000000031ec77000)] + 0x0000000107ada400 JavaThread "sentinel-time-tick-thread" daemon [_thread_blocked, id=32027, stack(0x000000031ec80000,0x000000031ee83000)] + 0x000000011b77a400 JavaThread "sentinel-heartbeat-send-task-thread-2" daemon [_thread_blocked, id=53555, stack(0x000000031ee8c000,0x000000031f08f000)] + +Other Threads: + 0x0000000104d04d00 VMThread "VM Thread" [stack: 0x000000016c4d0000,0x000000016c6d3000] [id=19715] + 0x0000000104c0a4f0 WatcherThread [stack: 0x000000016f914000,0x000000016fb17000] [id=34819] + 0x0000000104e067f0 GCTaskThread "GC Thread#0" [stack: 0x000000016ba94000,0x000000016bc97000] [id=12547] + 0x00000001051109e0 GCTaskThread "GC Thread#1" [stack: 0x000000016e49c000,0x000000016e69f000] [id=25859] + 0x0000000105214660 GCTaskThread "GC Thread#2" [stack: 0x000000016e6a8000,0x000000016e8ab000] [id=27395] + 0x000000010de05990 GCTaskThread "GC Thread#3" [stack: 0x000000016e8b4000,0x000000016eab7000] [id=26883] + 0x0000000105110e70 GCTaskThread "GC Thread#4" [stack: 0x000000016eac0000,0x000000016ecc3000] [id=26115] + 0x00000001051116f0 GCTaskThread "GC Thread#5" [stack: 0x000000016eccc000,0x000000016eecf000] [id=32771] + 0x0000000105111f70 GCTaskThread "GC Thread#6" [stack: 0x000000016eed8000,0x000000016f0db000] [id=43011] + 0x00000001051127f0 GCTaskThread "GC Thread#7" [stack: 0x000000016f0e4000,0x000000016f2e7000] [id=33283] + 0x0000000105113070 GCTaskThread "GC Thread#8" [stack: 0x000000016f2f0000,0x000000016f4f3000] [id=33795] + 0x0000000104809f30 GCTaskThread "GC Thread#9" [stack: 0x000000016f4fc000,0x000000016f6ff000] [id=42755] + 0x00000001050042b0 ConcurrentGCThread "G1 Main Marker" [stack: 0x000000016bca0000,0x000000016bea3000] [id=14083] + 0x000000011df045c0 ConcurrentGCThread "G1 Conc#0" [stack: 0x000000016beac000,0x000000016c0af000] [id=13827] + 0x000000010512ad30 ConcurrentGCThread "G1 Conc#1" [stack: 0x0000000319270000,0x0000000319473000] [id=39427] + 0x0000000104e11120 ConcurrentGCThread "G1 Conc#2" [stack: 0x000000031947c000,0x000000031967f000] [id=38403] + 0x0000000105105c00 ConcurrentGCThread "G1 Refine#0" [stack: 0x000000016c0b8000,0x000000016c2bb000] [id=16643] + 0x0000000105204080 ConcurrentGCThread "G1 Service" [stack: 0x000000016c2c4000,0x000000016c4c7000] [id=21251] + +Threads with active compile tasks: + +VM state: not at safepoint (normal execution) + +VM Mutex/Monitor currently owned by a thread: None + +Heap address: 0x00000005c0000000, size: 9216 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 + +CDS archive(s) mapped at: [0x000000e000000000-0x000000e000c14000-0x000000e000c14000), size 12664832, SharedBaseAddress: 0x000000e000000000, ArchiveRelocationMode: 1. +Compressed class space mapped at: 0x000000e001000000-0x000000e041000000, reserved size: 1073741824 +Narrow klass base: 0x000000e000000000, Narrow klass shift: 0, Narrow klass range: 0x100000000 + +GC Precious Log: + CPUs: 12 total, 12 available + Memory: 36864M + Large Page Support: Disabled + NUMA Support: Disabled + Compressed Oops: Enabled (Zero based) + Heap Region Size: 8M + Heap Min Capacity: 8M + Heap Initial Capacity: 576M + Heap Max Capacity: 9G + Pre-touch: Disabled + Parallel Workers: 10 + Concurrent Workers: 3 + Concurrent Refinement Workers: 10 + Periodic GC: Disabled + +Heap: + garbage-first heap total 196608K, used 119331K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 10 young (81920K), 1 survivors (8192K) + Metaspace used 65956K, committed 66496K, reserved 1114112K + class space used 8750K, committed 9024K, reserved 1048576K + +Heap Regions: E=young(eden), S=young(survivor), O=old, HS=humongous(starts), HC=humongous(continues), CS=collection set, F=free, OA=open archive, CA=closed archive, TAMS=top-at-mark-start (previous, next) +| 0|0x00000005c0000000, 0x00000005c0800000, 0x00000005c0800000|100%| O| |TAMS 0x00000005c0800000, 0x00000005c0000000| Untracked +| 1|0x00000005c0800000, 0x00000005c1000000, 0x00000005c1000000|100%| O| |TAMS 0x00000005c1000000, 0x00000005c0800000| Untracked +| 2|0x00000005c1000000, 0x00000005c1800000, 0x00000005c1800000|100%| O| |TAMS 0x00000005c178d800, 0x00000005c1000000| Untracked +| 3|0x00000005c1800000, 0x00000005c1f6fc00, 0x00000005c2000000| 92%| O| |TAMS 0x00000005c1f6fc00, 0x00000005c1800000| Untracked +| 4|0x00000005c2000000, 0x00000005c2521200, 0x00000005c2800000| 64%| O| |TAMS 0x00000005c2000000, 0x00000005c2000000| Untracked +| 5|0x00000005c2800000, 0x00000005c2800000, 0x00000005c3000000| 0%| F| |TAMS 0x00000005c2800000, 0x00000005c2800000| Untracked +| 6|0x00000005c3000000, 0x00000005c3000000, 0x00000005c3800000| 0%| F| |TAMS 0x00000005c3000000, 0x00000005c3000000| Untracked +| 7|0x00000005c3800000, 0x00000005c3800000, 0x00000005c4000000| 0%| F| |TAMS 0x00000005c3800000, 0x00000005c3800000| Untracked +| 8|0x00000005c4000000, 0x00000005c4000000, 0x00000005c4800000| 0%| F| |TAMS 0x00000005c4000000, 0x00000005c4000000| Untracked +| 9|0x00000005c4800000, 0x00000005c4800000, 0x00000005c5000000| 0%| F| |TAMS 0x00000005c4800000, 0x00000005c4800000| Untracked +| 10|0x00000005c5000000, 0x00000005c5000000, 0x00000005c5800000| 0%| F| |TAMS 0x00000005c5000000, 0x00000005c5000000| Untracked +| 11|0x00000005c5800000, 0x00000005c5800000, 0x00000005c6000000| 0%| F| |TAMS 0x00000005c5800000, 0x00000005c5800000| Untracked +| 12|0x00000005c6000000, 0x00000005c64df298, 0x00000005c6800000| 60%| E| |TAMS 0x00000005c6000000, 0x00000005c6000000| Complete +| 13|0x00000005c6800000, 0x00000005c7000000, 0x00000005c7000000|100%| E|CS|TAMS 0x00000005c6800000, 0x00000005c6800000| Complete +| 14|0x00000005c7000000, 0x00000005c7800000, 0x00000005c7800000|100%| E|CS|TAMS 0x00000005c7000000, 0x00000005c7000000| Complete +| 15|0x00000005c7800000, 0x00000005c8000000, 0x00000005c8000000|100%| E|CS|TAMS 0x00000005c7800000, 0x00000005c7800000| Complete +| 16|0x00000005c8000000, 0x00000005c8800000, 0x00000005c8800000|100%| S|CS|TAMS 0x00000005c8000000, 0x00000005c8000000| Complete +| 17|0x00000005c8800000, 0x00000005c9000000, 0x00000005c9000000|100%| E|CS|TAMS 0x00000005c8800000, 0x00000005c8800000| Complete +| 18|0x00000005c9000000, 0x00000005c9800000, 0x00000005c9800000|100%| E|CS|TAMS 0x00000005c9000000, 0x00000005c9000000| Complete +| 19|0x00000005c9800000, 0x00000005ca000000, 0x00000005ca000000|100%| E|CS|TAMS 0x00000005c9800000, 0x00000005c9800000| Complete +| 68|0x00000005e2000000, 0x00000005e2800000, 0x00000005e2800000|100%| E|CS|TAMS 0x00000005e2000000, 0x00000005e2000000| Complete +| 71|0x00000005e3800000, 0x00000005e4000000, 0x00000005e4000000|100%| E|CS|TAMS 0x00000005e3800000, 0x00000005e3800000| Complete +|1150|0x00000007ff000000, 0x00000007ff778000, 0x00000007ff800000| 93%|OA| |TAMS 0x00000007ff778000, 0x00000007ff000000| Untracked +|1151|0x00000007ff800000, 0x00000007ff880000, 0x0000000800000000| 6%|CA| |TAMS 0x00000007ff880000, 0x00000007ff800000| Untracked + +Card table byte_map: [0x000000010c200000,0x000000010d400000] _byte_map_base: 0x0000000109400000 + +Marking Bits (Prev, Next): (CMBitMap*) 0x000000010f808250, (CMBitMap*) 0x000000010f808210 + Prev Bits: [0x0000000149000000, 0x0000000152000000) + Next Bits: [0x0000000140000000, 0x0000000149000000) + +Polling page: 0x0000000104698000 + +Metaspace: + +Usage: + Non-class: 55.87 MB used. + Class: 8.55 MB used. + Both: 64.41 MB used. + +Virtual space: + Non-class space: 64.00 MB reserved, 56.12 MB ( 88%) committed, 1 nodes. + Class space: 1.00 GB reserved, 8.81 MB ( <1%) committed, 1 nodes. + Both: 1.06 GB reserved, 64.94 MB ( 6%) committed. + +Chunk freelists: + Non-Class: 7.80 MB + Class: 7.22 MB + Both: 15.02 MB + +MaxMetaspaceSize: unlimited +CompressedClassSpaceSize: 1.00 GB +Initial GC threshold: 21.00 MB +Current GC threshold: 100.31 MB +CDS: on +MetaspaceReclaimPolicy: balanced + - commit_granule_bytes: 65536. + - commit_granule_words: 8192. + - virtual_space_node_default_size: 8388608. + - enlarge_chunks_in_place: 1. + - new_chunks_are_fully_committed: 0. + - uncommit_free_chunks: 1. + - use_allocation_guard: 0. + - handle_deallocations: 1. + + +Internal statistics: + +num_allocs_failed_limit: 17. +num_arena_births: 702. +num_arena_deaths: 2. +num_vsnodes_births: 2. +num_vsnodes_deaths: 0. +num_space_committed: 1039. +num_space_uncommitted: 0. +num_chunks_returned_to_freelist: 19. +num_chunks_taken_from_freelist: 2813. +num_chunk_merges: 12. +num_chunk_splits: 2108. +num_chunks_enlarged: 1687. +num_inconsistent_stats: 0. + +CodeCache: size=49152Kb used=11912Kb max_used=11912Kb free=37239Kb + bounds [0x0000000108000000, 0x0000000108bb0000, 0x000000010b000000] + total_blobs=6643 nmethods=6019 adapters=554 + compilation: enabled + stopped_count=0, restarted_count=0 + full_count=0 + +Compilation events (20 events): +Event: 7.622 Thread 0x000000011b00c400 6308 1 org.aspectj.internal.lang.reflect.PerClauseImpl::getKind (5 bytes) +Event: 7.622 Thread 0x000000011b00c400 nmethod 6308 0x0000000108b9ea10 code [0x0000000108b9eb80, 0x0000000108b9ec18] +Event: 7.622 Thread 0x000000011b00c400 6310 ! 1 jdk.proxy2.$Proxy142::annotationType (29 bytes) +Event: 7.622 Thread 0x000000011b00c400 nmethod 6310 0x0000000108b9ed10 code [0x0000000108b9eec0, 0x0000000108b9f0d8] +Event: 7.623 Thread 0x000000011b00c400 6311 1 java.lang.reflect.Field::getAnnotation (23 bytes) +Event: 7.623 Thread 0x000000011f65ba00 6312 1 org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory::getAspectMetadata (5 bytes) +Event: 7.623 Thread 0x000000011b00c400 nmethod 6311 0x0000000108b9f290 code [0x0000000108b9f480, 0x0000000108b9f778] +Event: 7.623 Thread 0x000000011f65ba00 nmethod 6312 0x0000000108b9f990 code [0x0000000108b9fb00, 0x0000000108b9fb98] +Event: 7.623 Thread 0x000000011b00c400 6313 1 org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory::findAspectJAnnotationOnMethod (43 bytes) +Event: 7.623 Thread 0x000000011b00c400 nmethod 6313 0x0000000108b9fc90 code [0x0000000108b9fe40, 0x0000000108ba0018] +Event: 7.627 Thread 0x000000011b00c400 6314 ! 1 jdk.proxy2.$Proxy84::annotationType (29 bytes) +Event: 7.627 Thread 0x000000011b00c400 nmethod 6314 0x0000000108ba0190 code [0x0000000108ba0340, 0x0000000108ba0558] +Event: 7.627 Thread 0x000000011b00c400 6315 1 java.util.concurrent.atomic.AtomicInteger::getAndAdd (12 bytes) +Event: 7.627 Thread 0x000000011b00c400 nmethod 6315 0x0000000108ba0710 code [0x0000000108ba0880, 0x0000000108ba0958] +Event: 7.703 Thread 0x000000011f65ba00 6320 1 java.util.regex.Pattern::unread (11 bytes) +Event: 7.703 Thread 0x000000011b00c400 6321 1 java.util.regex.Pattern::qtype (39 bytes) +Event: 7.704 Thread 0x000000011f65ba00 nmethod 6320 0x0000000108ba1610 code [0x0000000108ba1780, 0x0000000108ba1858] +Event: 7.704 Thread 0x000000011b00c400 nmethod 6321 0x0000000108ba1910 code [0x0000000108ba1b00, 0x0000000108ba1e38] +Event: 7.704 Thread 0x000000011b00c400 6322 1 jdk.internal.misc.Unsafe::putReferenceOpaque (9 bytes) +Event: 7.704 Thread 0x000000011b00c400 nmethod 6322 0x0000000108ba2010 code [0x0000000108ba2180, 0x0000000108ba2298] + +GC Heap History (20 events): +Event: 0.685 GC heap before +{Heap before GC invocations=2 (full 0): + garbage-first heap total 606208K, used 49439K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 4 young (32768K), 1 survivors (8192K) + Metaspace used 11758K, committed 11968K, reserved 1114112K + class space used 1313K, committed 1408K, reserved 1048576K +} +Event: 0.687 GC heap after +{Heap after GC invocations=3 (full 0): + garbage-first heap total 606208K, used 28495K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 11758K, committed 11968K, reserved 1114112K + class space used 1313K, committed 1408K, reserved 1048576K +} +Event: 1.150 GC heap before +{Heap before GC invocations=3 (full 0): + garbage-first heap total 606208K, used 77647K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 7 young (57344K), 1 survivors (8192K) + Metaspace used 19291K, committed 19584K, reserved 1114112K + class space used 2462K, committed 2560K, reserved 1048576K +} +Event: 1.153 GC heap after +{Heap after GC invocations=4 (full 0): + garbage-first heap total 606208K, used 31366K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 19291K, committed 19584K, reserved 1114112K + class space used 2462K, committed 2560K, reserved 1048576K +} +Event: 1.228 GC heap before +{Heap before GC invocations=4 (full 0): + garbage-first heap total 606208K, used 47750K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 4 young (32768K), 1 survivors (8192K) + Metaspace used 21191K, committed 21504K, reserved 1114112K + class space used 2665K, committed 2816K, reserved 1048576K +} +Event: 1.231 GC heap after +{Heap after GC invocations=5 (full 0): + garbage-first heap total 606208K, used 32744K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 21191K, committed 21504K, reserved 1114112K + class space used 2665K, committed 2816K, reserved 1048576K +} +Event: 2.119 GC heap before +{Heap before GC invocations=6 (full 0): + garbage-first heap total 221184K, used 106472K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 12 young (98304K), 1 survivors (8192K) + Metaspace used 35766K, committed 36096K, reserved 1114112K + class space used 4465K, committed 4608K, reserved 1048576K +} +Event: 2.122 GC heap after +{Heap after GC invocations=7 (full 0): + garbage-first heap total 221184K, used 35490K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 2 survivors (16384K) + Metaspace used 35766K, committed 36096K, reserved 1114112K + class space used 4465K, committed 4608K, reserved 1048576K +} +Event: 2.130 GC heap before +{Heap before GC invocations=7 (full 0): + garbage-first heap total 221184K, used 35490K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 3 young (24576K), 2 survivors (16384K) + Metaspace used 36067K, committed 36416K, reserved 1114112K + class space used 4504K, committed 4672K, reserved 1048576K +} +Event: 2.134 GC heap after +{Heap after GC invocations=8 (full 0): + garbage-first heap total 221184K, used 36545K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 36067K, committed 36416K, reserved 1114112K + class space used 4504K, committed 4672K, reserved 1048576K +} +Event: 2.147 GC heap before +{Heap before GC invocations=8 (full 0): + garbage-first heap total 221184K, used 36545K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 1 survivors (8192K) + Metaspace used 36418K, committed 36736K, reserved 1114112K + class space used 4549K, committed 4672K, reserved 1048576K +} +Event: 2.148 GC heap after +{Heap after GC invocations=9 (full 0): + garbage-first heap total 442368K, used 35602K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 36418K, committed 36736K, reserved 1114112K + class space used 4549K, committed 4672K, reserved 1048576K +} +Event: 3.154 GC heap before +{Heap before GC invocations=10 (full 0): + garbage-first heap total 196608K, used 158482K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 17 young (139264K), 1 survivors (8192K) + Metaspace used 48508K, committed 48960K, reserved 1114112K + class space used 6329K, committed 6528K, reserved 1048576K +} +Event: 3.158 GC heap after +{Heap after GC invocations=11 (full 0): + garbage-first heap total 196608K, used 40351K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 48508K, committed 48960K, reserved 1114112K + class space used 6329K, committed 6528K, reserved 1048576K +} +Event: 4.257 GC heap before +{Heap before GC invocations=11 (full 0): + garbage-first heap total 196608K, used 138655K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 13 young (106496K), 1 survivors (8192K) + Metaspace used 54014K, committed 54464K, reserved 1114112K + class space used 7010K, committed 7232K, reserved 1048576K +} +Event: 4.263 GC heap after +{Heap after GC invocations=12 (full 0): + garbage-first heap total 196608K, used 46705K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 2 survivors (16384K) + Metaspace used 54014K, committed 54464K, reserved 1114112K + class space used 7010K, committed 7232K, reserved 1048576K +} +Event: 5.515 GC heap before +{Heap before GC invocations=12 (full 0): + garbage-first heap total 196608K, used 136817K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 13 young (106496K), 2 survivors (16384K) + Metaspace used 58257K, committed 58624K, reserved 1114112K + class space used 7651K, committed 7808K, reserved 1048576K +} +Event: 5.523 GC heap after +{Heap after GC invocations=13 (full 0): + garbage-first heap total 196608K, used 52335K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 2 young (16384K), 2 survivors (16384K) + Metaspace used 58257K, committed 58624K, reserved 1114112K + class space used 7651K, committed 7808K, reserved 1048576K +} +Event: 6.372 GC heap before +{Heap before GC invocations=13 (full 0): + garbage-first heap total 196608K, used 85103K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 7 young (57344K), 2 survivors (16384K) + Metaspace used 61127K, committed 61568K, reserved 1114112K + class space used 8029K, committed 8256K, reserved 1048576K +} +Event: 6.376 GC heap after +{Heap after GC invocations=14 (full 0): + garbage-first heap total 196608K, used 53795K [0x00000005c0000000, 0x0000000800000000) + region size 8192K, 1 young (8192K), 1 survivors (8192K) + Metaspace used 61127K, committed 61568K, reserved 1114112K + class space used 8029K, committed 8256K, reserved 1048576K +} + +Dll operation events (11 events): +Event: 0.042 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjava.dylib +Event: 0.043 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +Event: 0.145 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libinstrument.dylib +Event: 0.147 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnio.dylib +Event: 0.149 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnet.dylib +Event: 0.169 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjimage.dylib +Event: 0.179 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +Event: 0.296 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement.dylib +Event: 0.309 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement_ext.dylib +Event: 0.423 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libextnet.dylib +Event: 5.147 Loaded shared library /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libverify.dylib + +Deoptimization events (20 events): +Event: 7.531 Thread 0x0000000107808a00 DEOPT PACKING pc=0x0000000108333ac8 sp=0x000000016ba89ac0 +Event: 7.531 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba897a0 mode 1 +Event: 7.531 Thread 0x0000000107808a00 DEOPT PACKING pc=0x00000001086c7750 sp=0x000000016ba89b60 +Event: 7.531 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba898e0 mode 1 +Event: 7.531 Thread 0x0000000107808a00 DEOPT PACKING pc=0x00000001082b8728 sp=0x000000016ba89240 +Event: 7.531 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba88ee0 mode 1 +Event: 7.531 Thread 0x0000000107808a00 DEOPT PACKING pc=0x00000001082b823c sp=0x000000016ba89310 +Event: 7.531 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba88fe0 mode 1 +Event: 7.531 Thread 0x0000000107808a00 DEOPT PACKING pc=0x0000000108333ac8 sp=0x000000016ba89ab0 +Event: 7.531 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba89790 mode 1 +Event: 7.531 Thread 0x0000000107808a00 DEOPT PACKING pc=0x00000001086c7750 sp=0x000000016ba89b50 +Event: 7.531 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba898d0 mode 1 +Event: 7.543 Thread 0x0000000107808a00 DEOPT PACKING pc=0x0000000108535ad4 sp=0x000000016ba89bb0 +Event: 7.543 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba89930 mode 1 +Event: 7.545 Thread 0x0000000107808a00 DEOPT PACKING pc=0x0000000108535ad4 sp=0x000000016ba89ba0 +Event: 7.545 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba89920 mode 1 +Event: 7.572 Thread 0x0000000107808a00 DEOPT PACKING pc=0x0000000108535ad4 sp=0x000000016ba89bb0 +Event: 7.572 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba89930 mode 1 +Event: 7.572 Thread 0x0000000107808a00 DEOPT PACKING pc=0x0000000108535ad4 sp=0x000000016ba89ba0 +Event: 7.572 Thread 0x0000000107808a00 DEOPT UNPACKING pc=0x000000010804777c sp=0x000000016ba89920 mode 1 + +Classes unloaded (1 events): +Event: 6.390 Thread 0x0000000104d04d00 Unloading class 0x000000e001554000 'SC' + +Classes redefined (1 events): +Event: 0.160 Thread 0x0000000104d04d00 redefined class name=java.lang.Throwable, count=1 + +Internal exceptions (20 events): +Event: 5.854 Thread 0x000000011b960000 Exception (0x00000005c9c7a940) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 6.357 Thread 0x000000011b960000 Exception (0x00000005c8b001d0) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 6.865 Thread 0x000000011b960000 Exception (0x00000005c9009658) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 7.070 Thread 0x0000000107808a00 Exception (0x00000005c8d01110) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.070 Thread 0x0000000107808a00 Exception (0x00000005c8d05e08) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.071 Thread 0x0000000107808a00 Exception (0x00000005c8d0b6a8) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.072 Thread 0x0000000107808a00 Exception (0x00000005c8d17440) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.177 Thread 0x0000000107808a00 Exception (0x00000005c7aaa528) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 7.178 Thread 0x0000000107808a00 Exception (0x00000005c7ab2338) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 7.187 Thread 0x0000000107808a00 Exception (0x00000005c7b50300) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 7.187 Thread 0x0000000107808a00 Exception (0x00000005c7b577f8) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 7.187 Thread 0x0000000107808a00 Exception (0x00000005c7b5fdb8) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 7.207 Thread 0x0000000107808a00 Exception (0x00000005c7b914a8) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 7.216 Thread 0x0000000107808a00 Exception (0x00000005c7b9a268) +thrown [src/hotspot/share/classfile/systemDictionary.cpp, line 248] +Event: 7.342 Thread 0x0000000107808a00 Exception (0x00000005c72e5358) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 759] +Event: 7.368 Thread 0x000000011b960000 Exception (0x00000005c7479150) +thrown [src/hotspot/share/runtime/reflection.cpp, line 1121] +Event: 7.479 Thread 0x0000000107808a00 Exception (0x00000005c69c4500) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 826] +Event: 7.525 Thread 0x0000000107808a00 Exception (0x00000005c6bbd418) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 826] +Event: 7.526 Thread 0x0000000107808a00 Exception (0x00000005c6bc3d28) +thrown [src/hotspot/share/interpreter/linkResolver.cpp, line 826] +Event: 7.704 Thread 0x000000011e51f600 Exception (0x00000005c6477ae0) +thrown [src/hotspot/share/prims/jni.cpp, line 516] + +VM Operations (20 events): +Event: 6.389 Executing VM operation: G1PauseRemark +Event: 6.394 Executing VM operation: G1PauseRemark done +Event: 6.401 Executing VM operation: G1PauseCleanup +Event: 6.401 Executing VM operation: G1PauseCleanup done +Event: 6.543 Executing VM operation: HandshakeAllThreads +Event: 6.543 Executing VM operation: HandshakeAllThreads done +Event: 6.555 Executing VM operation: HandshakeAllThreads +Event: 6.555 Executing VM operation: HandshakeAllThreads done +Event: 6.560 Executing VM operation: HandshakeAllThreads +Event: 6.560 Executing VM operation: HandshakeAllThreads done +Event: 7.076 Executing VM operation: HandshakeAllThreads +Event: 7.076 Executing VM operation: HandshakeAllThreads done +Event: 7.080 Executing VM operation: HandshakeAllThreads +Event: 7.080 Executing VM operation: HandshakeAllThreads done +Event: 7.096 Executing VM operation: HandshakeAllThreads +Event: 7.096 Executing VM operation: HandshakeAllThreads done +Event: 7.131 Executing VM operation: ICBufferFull +Event: 7.131 Executing VM operation: ICBufferFull done +Event: 7.466 Executing VM operation: ICBufferFull +Event: 7.466 Executing VM operation: ICBufferFull done + +Events (20 events): +Event: 7.703 loading class java/net/SocksSocketImpl$3 done +Event: 7.704 loading class sun/net/util/SocketExceptions +Event: 7.704 loading class sun/net/util/SocketExceptions done +Event: 7.704 Thread 0x000000011b77a400 Thread added: 0x000000011b77a400 +Event: 7.704 loading class java/lang/Throwable$WrappedPrintWriter +Event: 7.704 loading class java/lang/Throwable$WrappedPrintWriter done +Event: 7.704 Protecting memory [0x000000031ee8c000,0x000000031ee98000] with protection modes 0 +Event: 7.704 loading class com/intellij/rt/debugger/agent/CaptureStorage$9 +Event: 7.704 loading class com/intellij/rt/debugger/agent/CaptureStorage$Callable +Event: 7.704 loading class com/intellij/rt/debugger/agent/CaptureStorage$Callable done +Event: 7.704 loading class com/intellij/rt/debugger/agent/CaptureStorage$9 done +Event: 7.705 loading class jdk/internal/loader/BootLoader$PackageHelper$1 +Event: 7.705 loading class jdk/internal/loader/BootLoader$PackageHelper$1 done +Event: 7.705 loading class jdk/internal/loader/BootLoader$PackageHelper$2 +Event: 7.705 loading class jdk/internal/loader/BootLoader$PackageHelper$2 done +Event: 7.705 loading class java/util/jar/JarInputStream +Event: 7.705 loading class java/util/zip/ZipInputStream +Event: 7.705 loading class java/util/zip/ZipInputStream done +Event: 7.705 loading class java/util/jar/JarInputStream done +Event: 7.706 loading class com/intellij/rt/debugger/agent/CaptureStorage$StackData + + +Dynamic libraries: +0x0000000104638000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjli.dylib +0x0000000196c18000 /usr/lib/libz.1.dylib +0x0000000196cce000 /usr/lib/libSystem.B.dylib +0x0000000196cc8000 /usr/lib/system/libcache.dylib +0x0000000196c83000 /usr/lib/system/libcommonCrypto.dylib +0x0000000196cae000 /usr/lib/system/libcompiler_rt.dylib +0x0000000196ca3000 /usr/lib/system/libcopyfile.dylib +0x0000000186bb6000 /usr/lib/system/libcorecrypto.dylib +0x0000000186cb6000 /usr/lib/system/libdispatch.dylib +0x0000000186a53000 /usr/lib/system/libdyld.dylib +0x0000000196cbe000 /usr/lib/system/libkeymgr.dylib +0x0000000196c66000 /usr/lib/system/libmacho.dylib +0x0000000195ef9000 /usr/lib/system/libquarantine.dylib +0x0000000196cbb000 /usr/lib/system/libremovefile.dylib +0x000000018d629000 /usr/lib/system/libsystem_asl.dylib +0x0000000186b3c000 /usr/lib/system/libsystem_blocks.dylib +0x0000000186d01000 /usr/lib/system/libsystem_c.dylib +0x0000000196cb2000 /usr/lib/system/libsystem_collections.dylib +0x0000000194899000 /usr/lib/system/libsystem_configuration.dylib +0x0000000193487000 /usr/lib/system/libsystem_containermanager.dylib +0x0000000196698000 /usr/lib/system/libsystem_coreservices.dylib +0x000000018ae50000 /usr/lib/system/libsystem_darwin.dylib +0x000000028c8a4000 /usr/lib/system/libsystem_darwindirectory.dylib +0x0000000196cbf000 /usr/lib/system/libsystem_dnssd.dylib +0x000000028c8a8000 /usr/lib/system/libsystem_eligibility.dylib +0x0000000186cfe000 /usr/lib/system/libsystem_featureflags.dylib +0x0000000186e83000 /usr/lib/system/libsystem_info.dylib +0x0000000196c27000 /usr/lib/system/libsystem_m.dylib +0x0000000186c65000 /usr/lib/system/libsystem_malloc.dylib +0x000000018d58c000 /usr/lib/system/libsystem_networkextension.dylib +0x000000018b2bb000 /usr/lib/system/libsystem_notify.dylib +0x000000019489e000 /usr/lib/system/libsystem_sandbox.dylib +0x000000028c8b3000 /usr/lib/system/libsystem_sanitizers.dylib +0x0000000196cb7000 /usr/lib/system/libsystem_secinit.dylib +0x0000000186e2f000 /usr/lib/system/libsystem_kernel.dylib +0x0000000186e7a000 /usr/lib/system/libsystem_platform.dylib +0x0000000186e6d000 /usr/lib/system/libsystem_pthread.dylib +0x000000018f1e2000 /usr/lib/system/libsystem_symptoms.dylib +0x0000000186b95000 /usr/lib/system/libsystem_trace.dylib +0x000000028c8bb000 /usr/lib/system/libsystem_trial.dylib +0x0000000196c91000 /usr/lib/system/libunwind.dylib +0x0000000186b40000 /usr/lib/system/libxpc.dylib +0x0000000186a00000 /usr/lib/libobjc.A.dylib +0x0000000186eb3000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation +0x000000019a5c3000 /usr/lib/swift/libswiftCore.dylib +0x0000000186e14000 /usr/lib/libc++abi.dylib +0x000000028ac91000 /usr/lib/libRosetta.dylib +0x0000000186d83000 /usr/lib/libc++.1.dylib +0x0000000188722000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation +0x00000001a41a3000 /usr/lib/swift/libswiftObjectiveC.dylib +0x000000028c10d000 /usr/lib/libswiftPrespecialized.dylib +0x0000000188391000 /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration +0x0000000191703000 /System/Library/PrivateFrameworks/CoreAutoLayout.framework/Versions/A/CoreAutoLayout +0x0000000196cd0000 /usr/lib/libfakelink.dylib +0x0000000196f79000 /usr/lib/libcompression.dylib +0x000000018d1d6000 /System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork +0x0000000190b34000 /System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration +0x0000000196d23000 /usr/lib/libarchive.2.dylib +0x0000000190a39000 /usr/lib/libDiagnosticMessagesClient.dylib +0x000000018ab7a000 /usr/lib/libicucore.A.dylib +0x000000019174c000 /usr/lib/libxml2.2.dylib +0x000000019f452000 /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices +0x00000001948ac000 /usr/lib/liblangid.dylib +0x000000018b1d2000 /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit +0x000000019d0c4000 /System/Library/Frameworks/Combine.framework/Versions/A/Combine +0x000000023fff3000 /System/Library/PrivateFrameworks/CollectionsInternal.framework/Versions/A/CollectionsInternal +0x000000026c039000 /System/Library/PrivateFrameworks/ReflectionInternal.framework/Versions/A/ReflectionInternal +0x000000026cf8d000 /System/Library/PrivateFrameworks/RuntimeInternal.framework/Versions/A/RuntimeInternal +0x0000000196cd2000 /System/Library/PrivateFrameworks/SoftLinking.framework/Versions/A/SoftLinking +0x00000001b488c000 /usr/lib/swift/libswiftCoreFoundation.dylib +0x00000001b164f000 /usr/lib/swift/libswiftDarwin.dylib +0x00000001a11c9000 /usr/lib/swift/libswiftDispatch.dylib +0x00000001b48ed000 /usr/lib/swift/libswiftIOKit.dylib +0x000000028c550000 /usr/lib/swift/libswiftSystem.dylib +0x00000001b489f000 /usr/lib/swift/libswiftXPC.dylib +0x000000028c582000 /usr/lib/swift/libswift_Builtin_float.dylib +0x000000028c583000 /usr/lib/swift/libswift_Concurrency.dylib +0x000000028c60f000 /usr/lib/swift/libswift_DarwinFoundation1.dylib +0x000000028c6b3000 /usr/lib/swift/libswift_StringProcessing.dylib +0x00000001a41a7000 /usr/lib/swift/libswiftos.dylib +0x000000018b152000 /System/Library/PrivateFrameworks/CoreServicesInternal.framework/Versions/A/CoreServicesInternal +0x0000000196c9b000 /usr/lib/liboah.dylib +0x000000018a75a000 /System/Library/Frameworks/Security.framework/Versions/A/Security +0x00000001a35d7000 /System/Library/PrivateFrameworks/DiskImages.framework/Versions/A/DiskImages +0x00000001b10f3000 /System/Library/Frameworks/NetFS.framework/Versions/A/NetFS +0x00000001916c8000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents +0x000000018ae5a000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore +0x0000000190aa8000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata +0x000000019669f000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices +0x0000000196e1b000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit +0x000000018f15c000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE +0x0000000187412000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices +0x0000000198224000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices +0x00000001916d5000 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList +0x0000000196eae000 /usr/lib/libapple_nghttp2.dylib +0x000000018ed78000 /usr/lib/libsqlite3.dylib +0x000000018ef61000 /System/Library/Frameworks/Accounts.framework/Versions/A/Accounts +0x00000001a3819000 /System/Library/PrivateFrameworks/AppSupport.framework/Versions/A/AppSupport +0x00000001b363e000 /System/Library/Frameworks/AVFoundation.framework/Versions/A/AVFoundation +0x0000000190a09000 /System/Library/PrivateFrameworks/CoreAnalytics.framework/Versions/A/CoreAnalytics +0x000000018dc1c000 /System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics +0x000000019b2fe000 /System/Library/Frameworks/GSS.framework/Versions/A/GSS +0x00000001996e6000 /System/Library/PrivateFrameworks/InternationalSupport.framework/Versions/A/InternationalSupport +0x000000018f0f0000 /System/Library/PrivateFrameworks/RunningBoardServices.framework/Versions/A/RunningBoardServices +0x00000001a412b000 /System/Library/PrivateFrameworks/StreamingZip.framework/Versions/A/StreamingZip +0x000000018d5a7000 /usr/lib/libenergytrace.dylib +0x000000018f1eb000 /System/Library/Frameworks/Network.framework/Versions/A/Network +0x0000000195f21000 /usr/lib/libbsm.0.dylib +0x0000000196c6a000 /usr/lib/system/libkxld.dylib +0x000000023b5c5000 /System/Library/PrivateFrameworks/AppleKeyStore.framework/Versions/A/AppleKeyStore +0x000000028a993000 /usr/lib/libCoreEntitlements.dylib +0x0000000260705000 /System/Library/PrivateFrameworks/MessageSecurity.framework/Versions/A/MessageSecurity +0x000000018ed5c000 /System/Library/PrivateFrameworks/ProtocolBuffer.framework/Versions/A/ProtocolBuffer +0x00000001a05d8000 /System/Library/PrivateFrameworks/SymptomDiagnosticReporter.framework/Versions/A/SymptomDiagnosticReporter +0x0000000198469000 /System/Library/PrivateFrameworks/CrashReporterSupport.framework/Versions/A/CrashReporterSupport +0x000000018d5a9000 /usr/lib/libMobileGestalt.dylib +0x000000019667f000 /System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression +0x0000000195f09000 /usr/lib/libcoretls.dylib +0x000000019829a000 /usr/lib/libcoretls_cfhelpers.dylib +0x0000000196f73000 /usr/lib/libpam.2.dylib +0x0000000198310000 /usr/lib/libxar.1.dylib +0x000000019829c000 /System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS +0x0000000278713000 /System/Library/PrivateFrameworks/SwiftASN1Internal.framework/Versions/A/SwiftASN1Internal +0x000000019831f000 /usr/lib/libutil.dylib +0x00000001948a7000 /System/Library/PrivateFrameworks/AppleSystemInfo.framework/Versions/A/AppleSystemInfo +0x0000000195bd0000 /System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/Versions/A/IOMobileFramebuffer +0x00000001934c0000 /System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface +0x00000001a2f37000 /System/Library/PrivateFrameworks/CoreWiFi.framework/Versions/A/CoreWiFi +0x00000001b474c000 /System/Library/PrivateFrameworks/LoggingSupport.framework/Versions/A/LoggingSupport +0x000000019b361000 /System/Library/PrivateFrameworks/MobileAsset.framework/Versions/A/MobileAsset +0x00000001a05e8000 /System/Library/PrivateFrameworks/PowerLog.framework/Versions/A/PowerLog +0x00000001a1aaa000 /System/Library/PrivateFrameworks/Rapport.framework/Versions/A/Rapport +0x000000023416e000 /System/Library/Frameworks/SwiftData.framework/Versions/A/SwiftData +0x000000018cc0a000 /System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers +0x00000001918f5000 /System/Library/PrivateFrameworks/UserManagement.framework/Versions/A/UserManagement +0x000000018d0fd000 /usr/lib/libboringssl.dylib +0x000000018f1d0000 /usr/lib/libdns_services.dylib +0x00000001b3772000 /usr/lib/libquic.dylib +0x000000019a554000 /usr/lib/libusrtcp.dylib +0x000000023c47f000 /System/Library/PrivateFrameworks/AtomicsInternal.framework/Versions/A/AtomicsInternal +0x00000001dab32000 /System/Library/PrivateFrameworks/InternalSwiftProtobuf.framework/Versions/A/InternalSwiftProtobuf +0x000000028c3d7000 /usr/lib/swift/libswiftDistributed.dylib +0x000000028c400000 /usr/lib/swift/libswiftObservation.dylib +0x000000028c53c000 /usr/lib/swift/libswiftSynchronization.dylib +0x00000001948a5000 /System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary +0x000000023ccaf000 /System/Library/PrivateFrameworks/BiomeLibrary.framework/Versions/A/BiomeLibrary +0x00000001c38d5000 /System/Library/PrivateFrameworks/BiomeStreams.framework/Versions/A/BiomeStreams +0x00000001bf924000 /System/Library/PrivateFrameworks/BiomeFoundation.framework/Versions/A/BiomeFoundation +0x00000001c9b82000 /System/Library/PrivateFrameworks/BiomePubSub.framework/Versions/A/BiomePubSub +0x000000018e96e000 /System/Library/Frameworks/CoreData.framework/Versions/A/CoreData +0x00000001a510e000 /System/Library/PrivateFrameworks/ProactiveSupport.framework/Versions/A/ProactiveSupport +0x00000002361cc000 /System/Library/Frameworks/_LocationEssentials.framework/Versions/A/_LocationEssentials +0x000000019827b000 /usr/lib/liblzma.5.dylib +0x000000019f6d1000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate +0x0000000195e02000 /System/Library/PrivateFrameworks/MobileKeyBag.framework/Versions/A/MobileKeyBag +0x00000001a3b2d000 /System/Library/PrivateFrameworks/InternationalTextSearch.framework/Versions/A/InternationalTextSearch +0x00000001bbdf7000 /System/Library/PrivateFrameworks/SoftwareUpdateCoreSupport.framework/Versions/A/SoftwareUpdateCoreSupport +0x00000001c4374000 /System/Library/PrivateFrameworks/SoftwareUpdateCoreConnect.framework/Versions/A/SoftwareUpdateCoreConnect +0x00000001a36d1000 /System/Library/PrivateFrameworks/RemoteServiceDiscovery.framework/Versions/A/RemoteServiceDiscovery +0x00000001bb9ce000 /System/Library/PrivateFrameworks/MSUDataAccessor.framework/Versions/A/MSUDataAccessor +0x00000001b691f000 /usr/lib/libbootpolicy.dylib +0x00000001a36e8000 /System/Library/PrivateFrameworks/RemoteXPC.framework/Versions/A/RemoteXPC +0x00000001c37a9000 /usr/lib/libFDR.dylib +0x00000001c9784000 /usr/lib/libamsupport.dylib +0x000000028ac89000 /usr/lib/libReverseProxyDevice.dylib +0x000000023ae33000 /System/Library/PrivateFrameworks/AppleDeviceQuerySupport.framework/Versions/A/AppleDeviceQuerySupport +0x00000001cc94e000 /usr/lib/libpartition2_dynamic.dylib +0x0000000196e8a000 /System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce +0x000000028a83e000 /usr/lib/libAppleArchive.dylib +0x000000019668b000 /usr/lib/libbz2.1.0.dylib +0x0000000190b3e000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage +0x000000019f42d000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib +0x0000000198356000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib +0x0000000187916000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib +0x00000001a3b2c000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices +0x0000000191833000 /System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo +0x000000018e372000 /System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync +0x0000000189d9a000 /System/Library/Frameworks/CoreText.framework/Versions/A/CoreText +0x0000000193fb3000 /System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO +0x000000019af0e000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS +0x000000018e51a000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices +0x00000001995dc000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore +0x000000019b2c7000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD +0x000000019b2c2000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy +0x000000019aee0000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis +0x000000018d665000 /System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight +0x000000019398e000 /System/Library/PrivateFrameworks/FontServices.framework/libFontParser.dylib +0x000000018effe000 /System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard +0x00000001a152a000 /System/Library/PrivateFrameworks/BoardServices.framework/Versions/A/BoardServices +0x00000001a33f9000 /System/Library/PrivateFrameworks/BackBoardServices.framework/Versions/A/BackBoardServices +0x000000023cbb4000 /System/Library/PrivateFrameworks/BackBoardHIDEventFoundation.framework/Versions/A/BackBoardHIDEventFoundation +0x00000001898b6000 /System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay +0x0000000198f35000 /System/Library/Frameworks/VideoToolbox.framework/Versions/A/VideoToolbox +0x0000000196f71000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders +0x000000026af43000 /System/Library/PrivateFrameworks/ProDisplayLibrary.framework/Versions/A/ProDisplayLibrary +0x00000001a722e000 /System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/Versions/A/IOSurfaceAccelerator +0x00000001934e8000 /System/Library/Frameworks/Metal.framework/Versions/A/Metal +0x00000001934dd000 /System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator +0x00000001937ec000 /System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia +0x000000018d641000 /System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC +0x0000000198eed000 /System/Library/PrivateFrameworks/WatchdogClient.framework/Versions/A/WatchdogClient +0x0000000190f63000 /System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore +0x0000000198eef000 /System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport +0x00000001cc730000 /usr/lib/swift/libswiftAccelerate.dylib +0x00000001b486c000 /usr/lib/swift/libswiftCoreAudio.dylib +0x00000001d08c1000 /usr/lib/swift/libswiftCoreMedia.dylib +0x00000001c2862000 /usr/lib/swift/libswiftMetal.dylib +0x00000001d2074000 /usr/lib/swift/libswiftOSLog.dylib +0x00000001c7c88000 /usr/lib/swift/libswiftQuartzCore.dylib +0x00000001cc720000 /usr/lib/swift/libswiftUniformTypeIdentifiers.dylib +0x000000028c56a000 /usr/lib/swift/libswiftVideoToolbox.dylib +0x00000001b83e6000 /usr/lib/swift/libswiftsimd.dylib +0x00000001c9be3000 /System/Library/PrivateFrameworks/BiomeStorage.framework/Versions/A/BiomeStorage +0x00000002593f4000 /System/Library/PrivateFrameworks/IntelligencePlatformLibrary.framework/Versions/A/IntelligencePlatformLibrary +0x0000000269d07000 /System/Library/PrivateFrameworks/PoirotSchematizer.framework/Versions/A/PoirotSchematizer +0x000000023d56a000 /System/Library/PrivateFrameworks/BiomeSync.framework/Versions/A/BiomeSync +0x000000023cc95000 /System/Library/PrivateFrameworks/BiomeDSL.framework/Versions/A/BiomeDSL +0x00000001e2d3d000 /System/Library/PrivateFrameworks/FeatureFlags.framework/Versions/A/FeatureFlags +0x0000000269d75000 /System/Library/PrivateFrameworks/PoirotUDFs.framework/Versions/A/PoirotUDFs +0x000000028c612000 /usr/lib/swift/libswift_DarwinFoundation2.dylib +0x000000028c613000 /usr/lib/swift/libswift_DarwinFoundation3.dylib +0x00000001a1a9f000 /System/Library/PrivateFrameworks/CoreTime.framework/Versions/A/CoreTime +0x0000000196d08000 /usr/lib/libiconv.2.dylib +0x0000000196c65000 /usr/lib/libcharset.1.dylib +0x0000000269cca000 /System/Library/PrivateFrameworks/PoirotSQLite.framework/Versions/A/PoirotSQLite +0x000000028c614000 /usr/lib/swift/libswift_RegexParser.dylib +0x000000023eba0000 /System/Library/PrivateFrameworks/CascadeSets.framework/Versions/A/CascadeSets +0x000000019b4d8000 /System/Library/PrivateFrameworks/CorePhoneNumbers.framework/Versions/A/CorePhoneNumbers +0x0000000198ce7000 /System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG +0x00000001986c0000 /usr/lib/libexpat.1.dylib +0x00000001994b2000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib +0x00000001994dd000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib +0x00000001995c5000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib +0x0000000198d2c000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib +0x00000001983d0000 /usr/lib/libate.dylib +0x000000019956c000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib +0x0000000199563000 /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib +0x000000024f044000 /System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/32023/Libraries/libllvm-flatbuffers.dylib +0x0000000249aa3000 /System/Library/PrivateFrameworks/FramePacing.framework/Versions/A/FramePacing +0x000000022cbb3000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib +0x000000024abd1000 /System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/32023/Libraries/libGPUCompilerUtils.dylib +0x00000001a15f7000 /System/Library/PrivateFrameworks/GraphicsServices.framework/Versions/A/GraphicsServices +0x000000022cbc1000 /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL +0x000000022cc12000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib +0x000000022cbd5000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib +0x000000022cda2000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib +0x000000022cbde000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib +0x000000022cbd2000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib +0x000000022cbbb000 /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib +0x000000019955e000 /System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler +0x000000019953e000 /System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment +0x0000000199566000 /System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay +0x00000002805c9000 /System/Library/PrivateFrameworks/VideoToolboxParavirtualizationSupport.framework/Versions/A/VideoToolboxParavirtualizationSupport +0x0000000198677000 /System/Library/PrivateFrameworks/AppleVA.framework/Versions/A/AppleVA +0x000000022ec3e000 /System/Library/Frameworks/ExtensionFoundation.framework/Versions/A/ExtensionFoundation +0x00000001995cb000 /System/Library/PrivateFrameworks/CMCaptureCore.framework/Versions/A/CMCaptureCore +0x0000000198951000 /usr/lib/libspindump.dylib +0x0000000189fc4000 /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio +0x0000000198944000 /System/Library/PrivateFrameworks/AppServerSupport.framework/Versions/A/AppServerSupport +0x000000019b2d0000 /System/Library/PrivateFrameworks/perfdata.framework/Versions/A/perfdata +0x00000001899d7000 /System/Library/PrivateFrameworks/AudioToolboxCore.framework/Versions/A/AudioToolboxCore +0x00000001937c2000 /System/Library/PrivateFrameworks/caulk.framework/Versions/A/caulk +0x000000019aec6000 /usr/lib/libAudioStatistics.dylib +0x00000001b3867000 /System/Library/PrivateFrameworks/SystemPolicy.framework/Versions/A/SystemPolicy +0x000000019b174000 /usr/lib/libSMC.dylib +0x00000001bb1dd000 /usr/lib/swift/libswiftCoreMIDI.dylib +0x00000001a651d000 /System/Library/Frameworks/CoreMIDI.framework/Versions/A/CoreMIDI +0x000000019948c000 /usr/lib/libAudioToolboxUtility.dylib +0x000000019b2de000 /usr/lib/libperfcheck.dylib +0x000000023c54e000 /System/Library/PrivateFrameworks/AudioAnalytics.framework/Versions/A/AudioAnalytics +0x00000001da81e000 /System/Library/Frameworks/OSLog.framework/Versions/A/OSLog +0x0000000265777000 /System/Library/PrivateFrameworks/OSEligibility.framework/Versions/A/OSEligibility +0x0000000198746000 /System/Library/PrivateFrameworks/IconServices.framework/Versions/A/IconServices +0x0000000230025000 /System/Library/Frameworks/LightweightCodeRequirements.framework/Versions/A/LightweightCodeRequirements +0x00000001985c0000 /System/Library/PrivateFrameworks/PlugInKit.framework/Versions/A/PlugInKit +0x0000000195e1a000 /System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices +0x00000001986e5000 /System/Library/PrivateFrameworks/IconFoundation.framework/Versions/A/IconFoundation +0x0000000255f80000 /System/Library/PrivateFrameworks/IconRendering.framework/Versions/A/IconRendering +0x00000001913ca000 /System/Library/PrivateFrameworks/CoreUI.framework/Versions/A/CoreUI +0x00000001942f4000 /System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage +0x000000026d172000 /System/Library/PrivateFrameworks/SFSymbols.framework/Versions/A/SFSymbols +0x000000022eaf4000 /System/Library/Frameworks/DeveloperToolsSupport.framework/Versions/A/DeveloperToolsSupport +0x00000001ab7ca000 /System/Library/PrivateFrameworks/RenderBox.framework/Versions/A/RenderBox +0x0000000193f75000 /System/Library/PrivateFrameworks/CoreSVG.framework/Versions/A/CoreSVG +0x000000019964f000 /System/Library/PrivateFrameworks/TextureIO.framework/Versions/A/TextureIO +0x00000001b48ec000 /usr/lib/swift/libswiftCoreImage.dylib +0x00000001988f4000 /System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer +0x00000002499ae000 /System/Library/PrivateFrameworks/FontServices.framework/Versions/A/FontServices +0x0000000198904000 /System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG +0x0000000191379000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib +0x000000028b763000 /usr/lib/libhvf.dylib +0x0000000266404000 /System/Library/PrivateFrameworks/ParsingInternal.framework/Versions/A/ParsingInternal +0x00000002499b2000 /System/Library/PrivateFrameworks/FontServices.framework/libXTFontStaticRegistryData.dylib +0x00000001947df000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSCore.framework/Versions/A/MPSCore +0x00000001965ea000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSImage.framework/Versions/A/MPSImage +0x0000000195fa9000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork +0x00000001963e8000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix +0x0000000196200000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector +0x000000019641a000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNDArray.framework/Versions/A/MPSNDArray +0x0000000230eaa000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSFunctions.framework/Versions/A/MPSFunctions +0x0000000230e8b000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSBenchmarkLoop.framework/Versions/A/MPSBenchmarkLoop +0x0000000230ebe000 /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSHost.framework/Versions/A/MPSHost +0x000000018772d000 /System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools +0x00000001b9b0d000 /System/Library/PrivateFrameworks/IOAccelMemoryInfo.framework/Versions/A/IOAccelMemoryInfo +0x00000001c807b000 /System/Library/PrivateFrameworks/kperf.framework/Versions/A/kperf +0x00000001b4868000 /System/Library/PrivateFrameworks/GPURawCounter.framework/Versions/A/GPURawCounter +0x00000001a5299000 /System/Library/PrivateFrameworks/ASEProcessing.framework/Versions/A/ASEProcessing +0x00000001d618a000 /System/Library/PrivateFrameworks/Symbolication.framework/Versions/A/Symbolication +0x00000002698b5000 /System/Library/PrivateFrameworks/PhotosensitivityProcessing.framework/Versions/A/PhotosensitivityProcessing +0x000000026d1f8000 /System/Library/PrivateFrameworks/SILManager.framework/Versions/A/SILManager +0x00000001a1943000 /System/Library/PrivateFrameworks/CoreSymbolication.framework/Versions/A/CoreSymbolication +0x00000001b47db000 /System/Library/PrivateFrameworks/MallocStackLogging.framework/Versions/A/MallocStackLogging +0x00000001a1921000 /System/Library/PrivateFrameworks/DebugSymbols.framework/Versions/A/DebugSymbols +0x00000001c67cc000 /System/Library/PrivateFrameworks/OSAnalytics.framework/Versions/A/OSAnalytics +0x0000000247709000 /System/Library/PrivateFrameworks/DeviceRecovery.framework/Versions/A/DeviceRecovery +0x000000027be51000 /System/Library/PrivateFrameworks/Tightbeam.framework/Versions/A/Tightbeam +0x00000001c2870000 /usr/lib/swift/libswiftCompression.dylib +0x00000001ccebd000 /System/Library/PrivateFrameworks/AFKUser.framework/Versions/A/AFKUser +0x0000000199597000 /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATSUI.framework/Versions/A/ATSUI +0x000000019ac6f000 /System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox +0x0000000196a5c000 /System/Library/Frameworks/UserNotifications.framework/Versions/A/UserNotifications +0x00000001ba2a4000 /System/Library/PrivateFrameworks/SiriInstrumentation.framework/Versions/A/SiriInstrumentation +0x000000026f688000 /System/Library/PrivateFrameworks/SiriAnalytics.framework/Versions/A/SiriAnalytics +0x00000001b7a03000 /System/Library/PrivateFrameworks/FeedbackLogger.framework/Versions/A/FeedbackLogger +0x00000001d230d000 /usr/lib/swift/libswiftAVFoundation.dylib +0x000000027e67a000 /System/Library/PrivateFrameworks/UnifiedAssetFramework.framework/Versions/A/UnifiedAssetFramework +0x000000019ae45000 /System/Library/PrivateFrameworks/AudioSession.framework/Versions/A/AudioSession +0x0000000198805000 /System/Library/PrivateFrameworks/MediaExperience.framework/Versions/A/MediaExperience +0x000000019ac19000 /System/Library/PrivateFrameworks/AudioSession.framework/libSessionUtility.dylib +0x00000001a04cd000 /System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth +0x0000000195c8d000 /System/Library/PrivateFrameworks/CoreUtils.framework/Versions/A/CoreUtils +0x00000001ac4fe000 /System/Library/PrivateFrameworks/HID.framework/Versions/A/HID +0x00000002465fa000 /System/Library/PrivateFrameworks/CoreUtilsExtras.framework/Versions/A/CoreUtilsExtras +0x0000000255ed5000 /System/Library/PrivateFrameworks/IO80211.framework/Versions/A/IO80211 +0x000000019ce10000 /System/Library/Frameworks/IOBluetooth.framework/Versions/A/IOBluetooth +0x000000028c41f000 /usr/lib/swift/libswiftRegexBuilder.dylib +0x0000000198460000 /usr/lib/libIOReport.dylib +0x00000001e2dc2000 /System/Library/PrivateFrameworks/WiFiPeerToPeer.framework/Versions/A/WiFiPeerToPeer +0x0000000195e29000 /System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation +0x000000023ec54000 /System/Library/PrivateFrameworks/Centauri.framework/Versions/A/Centauri +0x0000000188111000 /System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon +0x000000028bbb3000 /usr/lib/libmrc.dylib +0x0000000255f40000 /System/Library/PrivateFrameworks/IPConfiguration.framework/Versions/A/IPConfiguration +0x00000001d6961000 /System/Library/PrivateFrameworks/Netrb.framework/Versions/A/Netrb +0x00000001a1450000 /System/Library/PrivateFrameworks/FrontBoardServices.framework/Versions/A/FrontBoardServices +0x0000000195f92000 /usr/lib/libgermantok.dylib +0x00000001949ce000 /System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData +0x00000001a06d6000 /System/Library/PrivateFrameworks/MediaKit.framework/Versions/A/MediaKit +0x00000001a0624000 /System/Library/Frameworks/DiscRecording.framework/Versions/A/DiscRecording +0x00000001986db000 /usr/lib/libheimdal-asn1.dylib +0x00000001a4101000 /System/Library/Frameworks/AudioUnit.framework/Versions/A/AudioUnit +0x0000000191690000 /System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory +0x000000019169e000 /System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory +0x000000019d1b8000 /System/Library/PrivateFrameworks/GeoServices.framework/Versions/A/GeoServices +0x000000019abdf000 /System/Library/PrivateFrameworks/LocationSupport.framework/Versions/A/LocationSupport +0x0000000252bfe000 /System/Library/PrivateFrameworks/GeoServicesCore.framework/Versions/A/GeoServicesCore +0x00000001ad402000 /System/Library/PrivateFrameworks/PhoneNumbers.framework/Versions/A/PhoneNumbers +0x000000025b386000 /System/Library/PrivateFrameworks/LocationLogEncryption.framework/Versions/A/LocationLogEncryption +0x000000022d12e000 /System/Library/Frameworks/AVFAudio.framework/Versions/A/AVFAudio +0x000000022d272000 /System/Library/Frameworks/AVRouting.framework/Versions/A/AVRouting +0x00000001ad51a000 /usr/lib/libAccessibility.dylib +0x0000000259d70000 /System/Library/PrivateFrameworks/IsolatedCoreAudioClient.framework/Versions/A/IsolatedCoreAudioClient +0x00000002423ee000 /System/Library/PrivateFrameworks/CoreAudioOrchestration.framework/Versions/A/CoreAudioOrchestration +0x0000000199ab3000 /System/Library/Frameworks/MediaToolbox.framework/Versions/A/MediaToolbox +0x00000001a07fc000 /System/Library/PrivateFrameworks/CoreAVCHD.framework/Versions/A/CoreAVCHD +0x000000019f720000 /System/Library/Frameworks/MediaAccessibility.framework/Versions/A/MediaAccessibility +0x00000001a07f8000 /System/Library/PrivateFrameworks/Mangrove.framework/Versions/A/Mangrove +0x000000023e290000 /System/Library/PrivateFrameworks/CMPhoto.framework/Versions/A/CMPhoto +0x00000001a0fc5000 /System/Library/Frameworks/CoreTelephony.framework/Versions/A/CoreTelephony +0x00000001a07eb000 /System/Library/PrivateFrameworks/CoreAUC.framework/Versions/A/CoreAUC +0x000000023b3f6000 /System/Library/PrivateFrameworks/AppleJPEGXL.framework/Versions/A/AppleJPEGXL +0x000000019b4e8000 /usr/lib/libTelephonyUtilDynamic.dylib +0x00000001dd93f000 /System/Library/Frameworks/CryptoKit.framework/Versions/A/CryptoKit +0x00000001a40fc000 /System/Library/PrivateFrameworks/CryptoKitCBridging.framework/Versions/A/CryptoKitCBridging +0x00000001a1609000 /System/Library/Frameworks/CryptoTokenKit.framework/Versions/A/CryptoTokenKit +0x0000000247179000 /System/Library/PrivateFrameworks/Dendrite.framework/Versions/A/Dendrite +0x00000001b440c000 /System/Library/Frameworks/NaturalLanguage.framework/Versions/A/NaturalLanguage +0x0000000252901000 /System/Library/PrivateFrameworks/GenerativeModels.framework/Versions/A/GenerativeModels +0x00000001e2d49000 /usr/lib/swift/libswiftNaturalLanguage.dylib +0x000000023bf6d000 /System/Library/PrivateFrameworks/AppleMobileFileIntegrity.framework/Versions/A/AppleMobileFileIntegrity +0x000000028ad01000 /usr/lib/libTLE.dylib +0x00000001b480d000 /usr/lib/libmis.dylib +0x00000001ec491000 /System/Library/PrivateFrameworks/ConfigProfileHelper.framework/Versions/A/ConfigProfileHelper +0x00000001a428b000 /System/Library/PrivateFrameworks/Espresso.framework/Versions/A/Espresso +0x0000000191e20000 /System/Library/Frameworks/CoreML.framework/Versions/A/CoreML +0x00000001e0bf5000 /usr/lib/libedit.3.dylib +0x0000000229f3c000 /System/Library/PrivateFrameworks/ANECompiler.framework/Versions/A/ANECompiler +0x00000001a6361000 /System/Library/PrivateFrameworks/AppleNeuralEngine.framework/Versions/A/AppleNeuralEngine +0x000000025b4a4000 /System/Library/PrivateFrameworks/MIL.framework/Versions/A/MIL +0x0000000230ec4000 /System/Library/Frameworks/MetalPerformanceShadersGraph.framework/Versions/A/MetalPerformanceShadersGraph +0x000000025bc13000 /System/Library/PrivateFrameworks/MLCompilerServices.framework/Versions/A/MLCompilerServices +0x00000001a50dd000 /System/Library/PrivateFrameworks/ANEServices.framework/Versions/A/ANEServices +0x00000001b9ad0000 /usr/lib/libncurses.5.4.dylib +0x000000018b2ce000 /usr/lib/libsandbox.1.dylib +0x0000000198601000 /usr/lib/libMatch.1.dylib +0x00000002654f9000 /System/Library/PrivateFrameworks/ODIE.framework/Versions/A/ODIE +0x000000025e8a0000 /System/Library/PrivateFrameworks/MLModelAsset.framework/Versions/A/MLModelAsset +0x000000025bbb9000 /System/Library/PrivateFrameworks/MLCompilerRuntime.framework/Versions/A/MLCompilerRuntime +0x0000000196255000 /System/Library/Frameworks/MLCompute.framework/Versions/A/MLCompute +0x000000025bb3b000 /System/Library/PrivateFrameworks/MLAssetIO.framework/Versions/A/MLAssetIO +0x000000028c3f1000 /usr/lib/swift/libswiftMLCompute.dylib +0x00000001a11e0000 /System/Library/PrivateFrameworks/AVFCore.framework/Versions/A/AVFCore +0x00000001aae1b000 /System/Library/PrivateFrameworks/AVFCapture.framework/Versions/A/AVFCapture +0x000000023e087000 /System/Library/PrivateFrameworks/CMImaging.framework/Versions/A/CMImaging +0x00000001ab05d000 /System/Library/PrivateFrameworks/Quagga.framework/Versions/A/Quagga +0x00000001ab18e000 /System/Library/PrivateFrameworks/CMCapture.framework/Versions/A/CMCapture +0x000000019b071000 /System/Library/Frameworks/CoreMediaIO.framework/Versions/A/CoreMediaIO +0x000000023dfc2000 /System/Library/PrivateFrameworks/CMCaptureDevice.framework/Versions/A/CMCaptureDevice +0x0000000198a3d000 /System/Library/PrivateFrameworks/CoreBrightness.framework/Versions/A/CoreBrightness +0x000000023f14b000 /System/Library/PrivateFrameworks/CinematicFraming.framework/Versions/A/CinematicFraming +0x00000002619dd000 /System/Library/PrivateFrameworks/ModelManagerServices.framework/Versions/A/ModelManagerServices +0x00000001cf375000 /System/Library/PrivateFrameworks/CPMS.framework/Versions/A/CPMS +0x0000000279581000 /System/Library/PrivateFrameworks/SystemStatus.framework/Versions/A/SystemStatus +0x00000001b323c000 /System/Library/Frameworks/CoreMotion.framework/Versions/A/CoreMotion +0x00000001c3854000 /System/Library/PrivateFrameworks/TimeSync.framework/Versions/A/TimeSync +0x0000000247b41000 /System/Library/PrivateFrameworks/DistributedSensing.framework/Versions/A/DistributedSensing +0x00000001bec33000 /System/Library/PrivateFrameworks/MobileBluetooth.framework/Versions/A/MobileBluetooth +0x00000001c5518000 /System/Library/PrivateFrameworks/IOKitten.framework/Versions/A/IOKitten +0x000000023b270000 /System/Library/PrivateFrameworks/AppleIntelligenceReporting.framework/Versions/A/AppleIntelligenceReporting +0x0000000195b9c000 /System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji +0x0000000188425000 /usr/lib/libCRFSuite.dylib +0x0000000189706000 /System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling +0x00000001948ae000 /System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP +0x000000018e683000 /System/Library/PrivateFrameworks/Montreal.framework/Versions/A/Montreal +0x0000000196d10000 /usr/lib/libcmph.dylib +0x0000000195f33000 /usr/lib/libmecab.dylib +0x0000000196e81000 /usr/lib/libThaiTokenizer.dylib +0x00000002529e3000 /System/Library/PrivateFrameworks/GenerativeModelsFoundation.framework/Versions/A/GenerativeModelsFoundation +0x000000027c356000 /System/Library/PrivateFrameworks/TokenGeneration.framework/Versions/A/TokenGeneration +0x00000002527b7000 /System/Library/PrivateFrameworks/GenerativeFunctions.framework/Versions/A/GenerativeFunctions +0x0000000252805000 /System/Library/PrivateFrameworks/GenerativeFunctionsFoundation.framework/Versions/A/GenerativeFunctionsFoundation +0x000000026169e000 /System/Library/PrivateFrameworks/ModelCatalog.framework/Versions/A/ModelCatalog +0x000000026e8a2000 /System/Library/PrivateFrameworks/SensitiveContentAnalysisML.framework/Versions/A/SensitiveContentAnalysisML +0x000000025289b000 /System/Library/PrivateFrameworks/GenerativeFunctionsInstrumentation.framework/Versions/A/GenerativeFunctionsInstrumentation +0x000000026b6cb000 /System/Library/PrivateFrameworks/PromptKit.framework/Versions/A/PromptKit +0x000000026b0e1000 /System/Library/PrivateFrameworks/ProactiveDaemonSupport.framework/Versions/A/ProactiveDaemonSupport +0x000000027c58e000 /System/Library/PrivateFrameworks/TokenGenerationCore.framework/Versions/A/TokenGenerationCore +0x00000001b52db000 /System/Library/PrivateFrameworks/Trial.framework/Versions/A/Trial +0x00000001b525c000 /System/Library/PrivateFrameworks/TrialProto.framework/Versions/A/TrialProto +0x000000023ae9a000 /System/Library/PrivateFrameworks/AppleFlatBuffers.framework/Versions/A/AppleFlatBuffers +0x000000026eb72000 /System/Library/PrivateFrameworks/SentencePieceInternal.framework/Versions/A/SentencePieceInternal +0x000000019f77a000 /System/Library/Frameworks/Vision.framework/Versions/A/Vision +0x0000000246298000 /System/Library/PrivateFrameworks/CoreSceneUnderstanding.framework/Versions/A/CoreSceneUnderstanding +0x00000002811df000 /System/Library/PrivateFrameworks/VisionCore.framework/Versions/A/VisionCore +0x00000001999f0000 /System/Library/PrivateFrameworks/DataDetectorsCore.framework/Versions/A/DataDetectorsCore +0x00000001bdbc1000 /System/Library/Frameworks/Vision.framework/libfaceCore.dylib +0x00000001be6db000 /System/Library/PrivateFrameworks/Futhark.framework/Versions/A/Futhark +0x00000001c2629000 /System/Library/PrivateFrameworks/InertiaCam.framework/Versions/A/InertiaCam +0x00000001be468000 /System/Library/PrivateFrameworks/TextRecognition.framework/Versions/A/TextRecognition +0x000000022eadd000 /System/Library/Frameworks/DataDetection.framework/Versions/A/DataDetection +0x00000001b8674000 /System/Library/PrivateFrameworks/TextInput.framework/Versions/A/TextInput +0x000000019849e000 /System/Library/PrivateFrameworks/CVNLP.framework/Versions/A/CVNLP +0x00000001dad09000 /System/Library/PrivateFrameworks/HIDDisplay.framework/Versions/A/HIDDisplay +0x000000019b255000 /usr/lib/libcups.2.dylib +0x000000019b2ec000 /System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos +0x000000019af5c000 /usr/lib/libresolv.9.dylib +0x0000000198958000 /System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal +0x00000001a4100000 /System/Library/Frameworks/Kerberos.framework/Versions/A/Libraries/libHeimdalProxy.dylib +0x000000019b350000 /System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth +0x00000001ad40e000 /System/Library/PrivateFrameworks/AXCoreUtilities.framework/Versions/A/AXCoreUtilities +0x00000001bda0a000 /System/Library/PrivateFrameworks/AttributeGraph.framework/Versions/A/AttributeGraph +0x000000028a801000 /usr/lib/libAXSafeCategoryBundle.dylib +0x0000000235252000 /System/Library/Frameworks/TabularData.framework/Versions/A/TabularData +0x000000023c11d000 /System/Library/PrivateFrameworks/ArgumentParserInternal.framework/Versions/A/ArgumentParserInternal +0x0000000195a5e000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib +0x0000000197053000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib +0x0000000195f95000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib +0x0000000196ec7000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib +0x000000019704e000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib +0x00000001949d5000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib +0x0000000188221000 /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib +0x000000022e5c2000 /System/Library/Frameworks/CoreTransferable.framework/Versions/A/CoreTransferable +0x000000019b2b4000 /System/Library/PrivateFrameworks/NetAuth.framework/Versions/A/NetAuth +0x00000001918b4000 /System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport +0x000000018ca51000 /System/Library/PrivateFrameworks/UIFoundation.framework/Versions/A/UIFoundation +0x0000000195efd000 /usr/lib/libCheckFix.dylib +0x0000000190a3b000 /System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities +0x00000002569c7000 /System/Library/PrivateFrameworks/InstalledContentLibrary.framework/Versions/A/InstalledContentLibrary +0x000000018b192000 /System/Library/PrivateFrameworks/CoreServicesStore.framework/Versions/A/CoreServicesStore +0x00000001916ff000 /usr/lib/libapp_launch_measurement.dylib +0x00000001c8192000 /System/Library/PrivateFrameworks/MobileSystemServices.framework/Versions/A/MobileSystemServices +0x0000000198323000 /usr/lib/libxslt.1.dylib +0x0000000195ebc000 /System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement +0x00000001a3792000 /usr/lib/libcurl.4.dylib +0x000000028b517000 /usr/lib/libcrypto.46.dylib +0x000000028c09a000 /usr/lib/libssl.48.dylib +0x00000001a346c000 /System/Library/Frameworks/LDAP.framework/Versions/A/LDAP +0x00000001a34a8000 /System/Library/PrivateFrameworks/TrustEvaluationAgent.framework/Versions/A/TrustEvaluationAgent +0x000000019af79000 /usr/lib/libsasl2.2.dylib +0x00000001a6710000 /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa +0x000000018b32d000 /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit +0x000000023ffce000 /System/Library/PrivateFrameworks/CollectionViewCore.framework/Versions/A/CollectionViewCore +0x0000000193f6f000 /System/Library/PrivateFrameworks/XCTTargetBootstrap.framework/Versions/A/XCTTargetBootstrap +0x0000000199a42000 /System/Library/PrivateFrameworks/UserActivity.framework/Versions/A/UserActivity +0x0000000249ab0000 /System/Library/PrivateFrameworks/FrontBoard.framework/Versions/A/FrontBoard +0x000000027df9f000 /System/Library/PrivateFrameworks/UIIntelligenceSupport.framework/Versions/A/UIIntelligenceSupport +0x00000002342db000 /System/Library/Frameworks/SwiftUICore.framework/Versions/A/SwiftUICore +0x0000000284b4b000 /System/Library/PrivateFrameworks/WritingTools.framework/Versions/A/WritingTools +0x0000000283942000 /System/Library/PrivateFrameworks/WindowManagement.framework/Versions/A/WindowManagement +0x00000002497e0000 /System/Library/PrivateFrameworks/FocusEngine.framework/Versions/A/FocusEngine +0x00000002471e9000 /System/Library/PrivateFrameworks/DesignLibrary.framework/Versions/A/DesignLibrary +0x0000000193f5a000 /System/Library/PrivateFrameworks/DFRFoundation.framework/Versions/A/DFRFoundation +0x000000027eeec000 /System/Library/PrivateFrameworks/UpdateCycle.framework/Versions/A/UpdateCycle +0x0000000193c5e000 /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox +0x000000019f040000 /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/SpeechRecognition.framework/Versions/A/SpeechRecognition +0x0000000191686000 /System/Library/PrivateFrameworks/PerformanceAnalysis.framework/Versions/A/PerformanceAnalysis +0x000000019f3d0000 /System/Library/Frameworks/Accessibility.framework/Versions/A/Accessibility +0x0000000235238000 /System/Library/Frameworks/Symbols.framework/Versions/A/Symbols +0x0000000252dac000 /System/Library/PrivateFrameworks/Gestures.framework/Versions/A/Gestures +0x000000028c4c2000 /usr/lib/swift/libswiftSpatial.dylib +0x00000001b164e000 /usr/lib/swift/libswiftCoreGraphics.dylib +0x000000019fe14000 /usr/lib/swift/libswiftFoundation.dylib +0x00000001ebe32000 /usr/lib/swift/libswiftSwiftOnoneSupport.dylib +0x000000028c748000 /usr/lib/swift/libswiftsys_time.dylib +0x00000001d699f000 /System/Library/PrivateFrameworks/CoreMaterial.framework/Versions/A/CoreMaterial +0x000000028acfe000 /usr/lib/libSpatial.dylib +0x000000028a71e000 /System/Library/SubFrameworks/UIUtilities.framework/Versions/A/UIUtilities +0x00000001057c0000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/server/libjvm.dylib +0x00000001046ac000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjimage.dylib +0x00000001047a8000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjdwp.dylib +0x00000001046f4000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libjava.dylib +0x0000000104900000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libinstrument.dylib +0x00000001046d8000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libzip.dylib +0x000000028a7f3000 /usr/lib/i18n/libiconv_std.dylib +0x000000028a7e9000 /usr/lib/i18n/libUTF8.dylib +0x000000028a7f8000 /usr/lib/i18n/libmapper_none.dylib +0x00000001049d0000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libdt_socket.dylib +0x0000000104f6c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnio.dylib +0x0000000104fb0000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libnet.dylib +0x00000001049e4000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement.dylib +0x0000000104f4c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libmanagement_ext.dylib +0x0000000104f8c000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libextnet.dylib +0x0000000105300000 /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/libverify.dylib + + +VM Arguments: +jvm_args: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:49167,suspend=y,server=n -javaagent:/Users/liangxin/Library/Caches/JetBrains/IntelliJIdea2026.1/captureAgent/debugger-agent.jar=file:///var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/capture18007844485071508468.props -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Ddebugger.async.stack.trace.for.all.threads=true -Dfile.encoding=UTF-8 +java_command: org.springblade.resource.ResourceApplication +java_class_path (initial): /Users/liangxin/Project/JAVA/tms-erp-api/blade-ops/blade-resource/target/classes:/Users/liangxin/Project/JAVA/tms-erp-api/blade-common/target/classes:/Users/liangxin/.m2/repository/org/springblade/blade-core-launch/4.10.0.BASE-SNAPSHOT/blade-core-launch-4.10.0.BASE-SNAPSHOT.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.5.16/spring-boot-starter-web-3.5.16.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-starter-json/3.5.16/spring-boot-starter-json-3.5.16.jar:/Users/liangxin/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.21.4/jackson-datatype-jdk8-2.21.4.jar:/Users/liangxin/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.18.0/jackson-module-parameter-names-2.18.0.jar:/Users/liangxin/.m2/repository/org/springframework/spring-webmvc/6.2.19/spring-webmvc-6.2.19.jar:/Users/liangxin/.m2/repository/org/springframework/spring-context/6.2.19/spring-context-6.2.19.jar:/Users/liangxin/.m2/repository/org/springframework/spring-expression/6.2.19/spring-expression-6.2.19.jar:/Users/liangxin/.m2/repository/org/springframework/boot/spring-boot-starter-undertow/3.5.16/spring-boot-starter-undertow-3.5.16.jar:/Users/liangxin/.m2/repository/io/undertow/undertow-core/2.3.24.Final/undertow-core-2.3.24.Final.jar:/Users/liangxin/.m2/repository/org/jboss/xnio/xnio-api/3.8.16.Final/xnio-api-3.8.16.Final.jar:/Users/liangxin/.m2/repository/org/wildfly/common/wildfly-common/1.5.4.Final/wildfly-common-1.5.4.Final.jar:/Users/liangxin/.m2/repository/org/wildfly/client/wildfly-client-config/1.0.1.Final/wildfly-client-config-1.0.1.Final.jar:/Users/liangxin/.m2/repository/org/jboss/xnio/xnio-nio/3.8.16.Final/xnio-nio-3.8.16.Final.jar:/Users/liangxin/.m2/repository/org/jboss/threads/jboss-threads/3.7.0.Final/jboss-threads-3.7.0.Final.jar:/Users/liangxin/.m2/repository/io/smallrye/common/smallrye-common-annotation/2.6.0/smallrye-common-annotation-2.6.0.jar:/User +Launcher Type: SUN_STANDARD + +[Global flags] + intx CICompilerCount = 4 {product} {ergonomic} + uint ConcGCThreads = 3 {product} {ergonomic} + uint G1ConcRefinementThreads = 10 {product} {ergonomic} + size_t G1HeapRegionSize = 8388608 {product} {ergonomic} + uintx GCDrainStackTargetSize = 64 {product} {ergonomic} + size_t InitialHeapSize = 603979776 {product} {ergonomic} + bool ManagementServer = true {product} {command line} + size_t MarkStackSize = 4194304 {product} {ergonomic} + size_t MaxHeapSize = 9663676416 {product} {ergonomic} + size_t MaxNewSize = 5796528128 {product} {ergonomic} + size_t MinHeapDeltaBytes = 8388608 {product} {ergonomic} + size_t MinHeapSize = 8388608 {product} {ergonomic} + uintx NonProfiledCodeHeapSize = 0 {pd product} {ergonomic} + bool ProfileInterpreter = false {pd product} {command line} + uintx ProfiledCodeHeapSize = 0 {pd product} {ergonomic} + size_t SoftMaxHeapSize = 9663676416 {manageable} {ergonomic} + intx TieredStopAtLevel = 1 {product} {command line} + bool UseCompressedClassPointers = true {product lp64_product} {ergonomic} + bool UseCompressedOops = true {product lp64_product} {ergonomic} + bool UseG1GC = true {product} {ergonomic} + bool UseNUMA = false {product} {ergonomic} + bool UseNUMAInterleaving = false {product} {ergonomic} + +Logging: +Log output configuration: + #0: stdout all=warning uptime,level,tags + #1: stderr all=off uptime,level,tags + +Environment Variables: +JAVA_HOME=/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home +PATH=/Users/liangxin/ai-infra/.venv/bin:/Users/liangxin/.nacos/bin:/Applications/Docker.app/Contents/Resources/bin:/Users/liangxin/Library/pnpm:/opt/homebrew/opt/ruby@3.2/bin:/opt/homebrew/opt/openssl@3/bin:/opt/miniconda3/bin:/opt/miniconda3/condabin:/usr/local/sbin:/usr/local/bin:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Users/liangxin/fvm/default/bin:/Applications/MAMP/bin/php/php8.1.13/bin:/opt/homebrew/opt/ruby@3.2/bin:/Users/liangxin/.nvm/versions/node/v20.18.3/bin:/Applications/apache-tomcat-9.0.78:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Users/liangxin/fvm/default/bin:/opt/homebrew/opt/libpng/bin:/Applications/pngquant:/Users/liangxin/AndroidSDK/platform-tools:/Users/liangxin/Library/Android/sdk/platform-tools:/Users/liangxin/Library/Andriod/sdk/cmdline-tools/latest/bin:/Users/liangxin/Library/Andriod/sdk:/Applications/apache-maven-3.8.1/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/local/bin:/Users/liangxin/JDK/zulu17.48.15-ca-jdk17.0.10-macosx_aarch64/zulu-17.jdk/Contents/Home/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/Users/liangxin/.local/bin:/Users/liangxin/fvm/default/bin:/Applications/MAMP/bin/php/php8.1.13/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/liangxin/.cargo/bin:true:/Applications/极空间.app/Contents/Resources/app.asar.unpacked/bin/platform-tools +SHELL=/bin/zsh +LANG=C.UTF-8 +TMPDIR=/var/folders/pk/drq93rk10q733kk02fgp89xh0000gn/T/ + +Active Locale: +LC_ALL=C.UTF-8 +LC_COLLATE=C.UTF-8 +LC_CTYPE=C.UTF-8 +LC_MESSAGES=C.UTF-8 +LC_MONETARY=C.UTF-8 +LC_NUMERIC=C.UTF-8 +LC_TIME=C.UTF-8 + +Signal Handlers: + SIGSEGV: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGBUS: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGFPE: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGPIPE: javaSignalHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGXFSZ: javaSignalHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGILL: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + SIGUSR2: SR_handler in libjvm.dylib, mask=00000000000000000000000000000000, flags=SA_RESTART|SA_SIGINFO, blocked + SIGHUP: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGINT: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGTERM: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGQUIT: UserHandler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, blocked + SIGTRAP: crash_handler in libjvm.dylib, mask=11100110000111110111111111111111, flags=SA_RESTART|SA_SIGINFO, unblocked + + +--------------- S Y S T E M --------------- + +OS: +uname: Darwin 25.6.0 Darwin Kernel Version 25.6.0: Fri Jul 31 19:16:36 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6030 arm64 +OS uptime: 2 days 5:15 hours +rlimit (soft/hard): STACK 8176k/65520k , CORE 0k/infinity , NPROC 6000/9000 , NOFILE 10240/infinity , AS infinity/infinity , CPU infinity/infinity , DATA infinity/infinity , FSIZE infinity/infinity , MEMLOCK infinity/infinity , RSS infinity/infinity +load average: 16.89 38.96 54.23 + +CPU: total 12 (initial active 12) 0x61:0x0:0x5f4dea93:0, fp, simd, crc, lse +machdep.cpu.brand_string:Apple M3 Pro +hw.cachelinesize:128 +hw.l1icachesize:131072 +hw.l1dcachesize:65536 +hw.l2cachesize:4194304 + +Memory: 16k page, physical 37748736k(215216k free), swap 18874368k(1062912k free) + +vm_info: OpenJDK 64-Bit Server VM (17.0.8+7-LTS) for bsd-aarch64 JRE (17.0.8+7-LTS) (Zulu17.44+15-CA), built on Jul 5 2023 00:50:04 by "zulu_re" with clang Apple LLVM 12.0.0 (clang-1200.0.32.28) + +END. diff --git a/pom.xml b/pom.xml index bb5f2bf..ae708e0 100644 --- a/pom.xml +++ b/pom.xml @@ -149,6 +149,11 @@ blade-track-api ${revision} + + org.springblade + blade-wechat-api + ${revision} + org.springblade From a2d9cfec028c81bdcfe0caee1d46afca254ac2d3 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 18 Sep 2026 19:13:45 +0800 Subject: [PATCH 105/114] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E6=89=8B=E6=9C=BA=E5=8F=B7=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=97=AE=E9=A2=98=202=E3=80=81=E8=B0=83=E6=95=B4OA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 5 + .../oa/config/OAFeignClientConfig.java | 30 +---- .../thirdparty/oa/config/OAProperties.java | 2 +- .../oa/interceptor/OARequestInterceptor.java | 122 ++++++++++++++++++ doc/nacos/blade-dev.yaml | 1 + doc/nacos/blade-prod.yaml | 1 + doc/nacos/third-party-api.yaml | 1 + 7 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 1d8d630..2d04e41 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -32,3 +32,8 @@ iam: authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50} + +# OA人员同步走同一 gwzh 网关,仅需 Authorization(与可用 curl 一致) +thirdParty: + oa: + authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}} diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java index 8917a2d..ef4306a 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java @@ -1,42 +1,26 @@ package org.springblade.thirdparty.oa.config; import feign.Request; -import feign.RequestInterceptor; -import jakarta.annotation.Resource; -import org.springblade.core.tool.utils.StringUtil; +import org.springblade.thirdparty.oa.interceptor.OARequestInterceptor; import org.springframework.context.annotation.Bean; import java.util.concurrent.TimeUnit; /** + * OA Feign 客户端配置 + * * @author bfhuange * @since 2024/12/18 */ public class OAFeignClientConfig { - @Resource - OAProperties oaProperties; - @Bean - public RequestInterceptor requestInterceptor() { - return template -> { - // 空实现屏蔽 全局拦截器 BladeFeignRequestInterceptor - template.header("Authorization", normalizeAuthorization(oaProperties.getAuthorization())); - }; - } + @Bean + public OARequestInterceptor requestInterceptor(OAProperties oaProperties) { + return new OARequestInterceptor(oaProperties); + } @Bean public Request.Options options() { return new Request.Options(10, TimeUnit.SECONDS, 120, TimeUnit.SECONDS, true); } - - private String normalizeAuthorization(String authorization) { - if (StringUtil.isBlank(authorization)) { - return authorization; - } - if (StringUtil.startsWithIgnoreCase(authorization, "Basic ") - || StringUtil.startsWithIgnoreCase(authorization, "Bearer ")) { - return authorization; - } - return "Basic " + authorization; - } } diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAProperties.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAProperties.java index bfcbae1..264d15b 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAProperties.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAProperties.java @@ -17,7 +17,7 @@ public class OAProperties { private String baseUrl; /** - * authorization + * gwzh 网关 Authorization(Basic),与可用 curl 一致 */ private String authorization; } diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java new file mode 100644 index 0000000..fa6e580 --- /dev/null +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java @@ -0,0 +1,122 @@ +/** + * 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.thirdparty.oa.interceptor; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.thirdparty.oa.config.OAProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * OA Feign 请求拦截器。 + *

+ * 对齐可用 curl:仅发送 Authorization + Content-Type,并清掉登录请求透传头, + * 避免 nginx 因 Host/Content-Length/Blade-Auth 等返回 400。 + * + * @author Chill + */ +@Slf4j +@RequiredArgsConstructor +public class OARequestInterceptor implements RequestInterceptor { + + private static final String BASIC_PREFIX = "Basic "; + private static final String BEARER_PREFIX = "Bearer "; + private static final Set STRIP_HEADERS = Set.of( + HttpHeaders.HOST.toLowerCase(Locale.ROOT), + HttpHeaders.CONTENT_LENGTH.toLowerCase(Locale.ROOT), + HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT), + HttpHeaders.ACCEPT.toLowerCase(Locale.ROOT), + HttpHeaders.CONNECTION.toLowerCase(Locale.ROOT), + HttpHeaders.TRANSFER_ENCODING.toLowerCase(Locale.ROOT), + HttpHeaders.COOKIE.toLowerCase(Locale.ROOT), + HttpHeaders.AUTHORIZATION.toLowerCase(Locale.ROOT), + HttpHeaders.ACCEPT_ENCODING.toLowerCase(Locale.ROOT), + HttpHeaders.ORIGIN.toLowerCase(Locale.ROOT), + HttpHeaders.REFERER.toLowerCase(Locale.ROOT), + HttpHeaders.EXPECT.toLowerCase(Locale.ROOT), + "keep-alive", + "upgrade", + "te", + "trailer", + "forwarded", + "x-real-ip", + "x-request-id", + "blade-auth", + "blade-requested-with", + "tenant-id", + "auth" + ); + + private final OAProperties oaProperties; + + @Override + public void apply(RequestTemplate template) { + stripForwardedHeaders(template); + // 与可用 curl 保持一致:Authorization + Content-Type + template.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + + String authorization = normalizeAuthorization(oaProperties.getAuthorization()); + if (StringUtil.isNotBlank(authorization)) { + template.header(HttpHeaders.AUTHORIZATION, authorization); + } else { + log.warn("OA Feign 未配置 third-party.oa.authorization,gwzh 网关可能拒绝请求"); + } + } + + private void stripForwardedHeaders(RequestTemplate template) { + List headerNames = new ArrayList<>(template.headers().keySet()); + for (String headerName : headerNames) { + String lowerName = headerName.toLowerCase(Locale.ROOT); + if (STRIP_HEADERS.contains(lowerName) + || lowerName.startsWith("x-forwarded-") + || lowerName.startsWith("blade-") + || lowerName.startsWith("x-b3-") + || lowerName.startsWith("sw8")) { + template.removeHeader(headerName); + } + } + } + + private String normalizeAuthorization(String authorization) { + if (StringUtil.isBlank(authorization)) { + return authorization; + } + if (StringUtil.startsWithIgnoreCase(authorization, BASIC_PREFIX) + || StringUtil.startsWithIgnoreCase(authorization, BEARER_PREFIX)) { + return authorization; + } + return BASIC_PREFIX + authorization; + } +} diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index 562e977..a2763a4 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -88,6 +88,7 @@ thirdParty: # OA开放接口地址 baseUrl: http://127.0.0.1:8080 queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST + authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}} track: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 69b2ffa..7279339 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -60,6 +60,7 @@ thirdParty: # OA开放接口地址 baseUrl: http://127.0.0.1:8080 queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST + authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}} track: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index a3fffa0..baa53a0 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -12,6 +12,7 @@ thirdParty: # OA开放接口地址 baseUrl: ${OA_BASE_URL:http://127.0.0.1:8080} queryPersonPageUrl: ${OA_QUERY_PERSON_PAGE_URL:/gwzh/OA/OA_GET_USER_LIST} + authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:}} track: # 轨迹开放接口地址 baseUrl: ${TRACK_BASE_URL:http://127.0.0.1:8080} From 8d13978e848c10d93d012263da2904e0d8badac8 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 18 Sep 2026 19:48:16 +0800 Subject: [PATCH 106/114] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E6=89=8B=E6=9C=BA=E5=8F=B7=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=97=AE=E9=A2=98=202=E3=80=81=E8=B0=83=E6=95=B4OA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/controller/UserController.java | 2 +- .../service/impl/OASyncServiceImpl.java | 4 +- .../oa/config/OAFeignClientConfig.java | 23 +++++- .../oa/interceptor/OARequestInterceptor.java | 73 ++++++++++--------- 4 files changed, 63 insertions(+), 39 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java index 36c7f7d..ab5e782 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java @@ -169,7 +169,7 @@ public class UserController { @Operation(summary = "同步OA人员") public R syncIamAccounts( @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "50") Integer size) { + @RequestParam(defaultValue = "20") Integer size) { return R.data(oaSyncService.syncPersonFromUserList(current, size)); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 6e0c3e9..0a02139 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -266,7 +266,7 @@ public class OASyncServiceImpl implements IOASyncService { */ private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size) { int pageNo = current < 1 ? 1 : current; - int pageSize = size < 1 ? 50 : Math.min(size, 200); + int pageSize = size < 1 ? 20 : Math.min(size, 200); OAPersonSearch personSearch = buildPersonSearch(null); personSearch.setCurPage(pageNo); personSearch.setPageSize(pageSize); @@ -309,7 +309,7 @@ public class OASyncServiceImpl implements IOASyncService { private OAPersonSearch buildPersonSearch(Date startTime) { OAPersonSearch personSearch = new OAPersonSearch(); personSearch.setCurPage(1); - personSearch.setPageSize(200); + personSearch.setPageSize(20); personSearch.setCreated(""); personSearch.setWorkcode(""); personSearch.setSubcompanyid1(""); diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java index ef4306a..c803caa 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/config/OAFeignClientConfig.java @@ -1,24 +1,45 @@ package org.springblade.thirdparty.oa.config; +import feign.Logger; import feign.Request; import org.springblade.thirdparty.oa.interceptor.OARequestInterceptor; +import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; import org.springframework.context.annotation.Bean; import java.util.concurrent.TimeUnit; /** - * OA Feign 客户端配置 + * OA Feign 客户端配置。 + *

+ * 必须关闭父上下文继承,否则全局 {@code BladeFeignRequestInterceptor} + * 会把当前登录请求的 Host、Content-Length、Blade-Auth 等头再次写入, + * 导致 gwzh nginx 返回 400。 * * @author bfhuange * @since 2024/12/18 */ public class OAFeignClientConfig { + @Bean + public FeignClientConfigurer feignClientConfigurer() { + return new FeignClientConfigurer() { + @Override + public boolean inheritParentConfiguration() { + return false; + } + }; + } + @Bean public OARequestInterceptor requestInterceptor(OAProperties oaProperties) { return new OARequestInterceptor(oaProperties); } + @Bean + public Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } + @Bean public Request.Options options() { return new Request.Options(10, TimeUnit.SECONDS, 120, TimeUnit.SECONDS, true); diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java index fa6e580..e3d7c17 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/interceptor/OARequestInterceptor.java @@ -34,16 +34,21 @@ import org.springblade.thirdparty.oa.config.OAProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; /** * OA Feign 请求拦截器。 *

- * 对齐可用 curl:仅发送 Authorization + Content-Type,并清掉登录请求透传头, - * 避免 nginx 因 Host/Content-Length/Blade-Auth 等返回 400。 + * 对齐可用 curl:仅发送 Authorization + Content-Type。 + * 配合 {@code FeignClientConfigurer#inheritParentConfiguration()=false}, + * 避免全局 BladeFeignRequestInterceptor 透传登录请求头。 * * @author Chill */ @@ -53,38 +58,16 @@ public class OARequestInterceptor implements RequestInterceptor { private static final String BASIC_PREFIX = "Basic "; private static final String BEARER_PREFIX = "Bearer "; - private static final Set STRIP_HEADERS = Set.of( - HttpHeaders.HOST.toLowerCase(Locale.ROOT), - HttpHeaders.CONTENT_LENGTH.toLowerCase(Locale.ROOT), - HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT), - HttpHeaders.ACCEPT.toLowerCase(Locale.ROOT), - HttpHeaders.CONNECTION.toLowerCase(Locale.ROOT), - HttpHeaders.TRANSFER_ENCODING.toLowerCase(Locale.ROOT), - HttpHeaders.COOKIE.toLowerCase(Locale.ROOT), + private static final Set KEEP_HEADERS = Set.of( HttpHeaders.AUTHORIZATION.toLowerCase(Locale.ROOT), - HttpHeaders.ACCEPT_ENCODING.toLowerCase(Locale.ROOT), - HttpHeaders.ORIGIN.toLowerCase(Locale.ROOT), - HttpHeaders.REFERER.toLowerCase(Locale.ROOT), - HttpHeaders.EXPECT.toLowerCase(Locale.ROOT), - "keep-alive", - "upgrade", - "te", - "trailer", - "forwarded", - "x-real-ip", - "x-request-id", - "blade-auth", - "blade-requested-with", - "tenant-id", - "auth" + HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT) ); private final OAProperties oaProperties; @Override public void apply(RequestTemplate template) { - stripForwardedHeaders(template); - // 与可用 curl 保持一致:Authorization + Content-Type + stripUnwantedHeaders(template); template.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); String authorization = normalizeAuthorization(oaProperties.getAuthorization()); @@ -93,17 +76,37 @@ public class OARequestInterceptor implements RequestInterceptor { } else { log.warn("OA Feign 未配置 third-party.oa.authorization,gwzh 网关可能拒绝请求"); } + + logRequest(template); } - private void stripForwardedHeaders(RequestTemplate template) { - List headerNames = new ArrayList<>(template.headers().keySet()); + /** + * 打印 OA Feign 最终发出的请求头与 body,便于对照 curl。 + */ + private void logRequest(RequestTemplate template) { + String bodyText = ""; + byte[] body = template.body(); + if (body != null && body.length > 0) { + Charset charset = template.requestCharset() == null ? StandardCharsets.UTF_8 : template.requestCharset(); + bodyText = new String(body, charset); + } + log.info("OA Feign 请求 method={}, url={}{}{}, headers={}, body={}", + template.method(), + template.feignTarget() == null ? "" : template.feignTarget().url(), + template.path(), + template.queryLine() == null ? "" : template.queryLine(), + template.headers(), + bodyText); + } + + /** + * 只保留 Authorization / Content-Type,其余全部移除。 + */ + private void stripUnwantedHeaders(RequestTemplate template) { + Map> headers = template.headers(); + List headerNames = new ArrayList<>(headers.keySet()); for (String headerName : headerNames) { - String lowerName = headerName.toLowerCase(Locale.ROOT); - if (STRIP_HEADERS.contains(lowerName) - || lowerName.startsWith("x-forwarded-") - || lowerName.startsWith("blade-") - || lowerName.startsWith("x-b3-") - || lowerName.startsWith("sw8")) { + if (!KEEP_HEADERS.contains(headerName.toLowerCase(Locale.ROOT))) { template.removeHeader(headerName); } } From 3b26591f9d94e2cbcf1f92c6b8057a4a4f640b97 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 18 Sep 2026 20:25:33 +0800 Subject: [PATCH 107/114] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=85=AC=E5=8F=B8=E3=80=81=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/pojo/vo/OaOrgSyncPageVO.java | 69 +++++ .../system/controller/DeptController.java | 37 ++- .../system/service/IOASyncService.java | 19 ++ .../service/impl/OASyncServiceImpl.java | 243 +++++++++++++++--- 4 files changed, 335 insertions(+), 33 deletions(-) create mode 100644 blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java new file mode 100644 index 0000000..590a8ba --- /dev/null +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java @@ -0,0 +1,69 @@ +/** + * 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.system.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * OA组织(公司/部门)分页同步结果 + * + * @author Chill + */ +@Data +@Schema(description = "OA组织分页同步结果") +public class OaOrgSyncPageVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "同步阶段:company / department") + private String stage; + + @Schema(description = "当前页") + private Integer current; + + @Schema(description = "每页条数") + private Integer size; + + @Schema(description = "OA总条数") + private Long total; + + @Schema(description = "本页从OA拉取的条数") + private Integer fetchedCount; + + @Schema(description = "本页同步成功条数") + private Integer syncedCount; + + @Schema(description = "本页跳过条数") + private Integer skippedCount; + + @Schema(description = "是否已到最后一页") + private Boolean finished; +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 9cccc5d..e407597 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -50,8 +50,10 @@ import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.User; import org.springblade.system.pojo.enums.DictEnum; import org.springblade.system.pojo.vo.DeptVO; +import org.springblade.system.pojo.vo.OaOrgSyncPageVO; import org.springblade.system.pojo.vo.UserVO; import org.springblade.system.service.IDeptService; +import org.springblade.system.service.IOASyncService; import org.springblade.system.wrapper.DeptWrapper; import org.springframework.web.bind.annotation.*; @@ -73,6 +75,7 @@ import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE; public class DeptController extends BladeController { private final IDeptService deptService; + private final IOASyncService oaSyncService; /** * 详情 @@ -170,12 +173,38 @@ public class DeptController extends BladeController { return R.data(deptService.syncIamOrganizations()); } + /** + * 从OA按页同步公司 + */ + @IsAdmin + @PostMapping("/sync-oa-company") + @ApiOperationSupport(order = 8) + @Operation(summary = "同步OA公司") + public R syncOaCompany( + @RequestParam(defaultValue = "1") Integer current, + @RequestParam(defaultValue = "50") Integer size) { + return R.data(oaSyncService.syncCompanyPage(current, size)); + } + + /** + * 从OA按页同步部门(需先完成公司同步) + */ + @IsAdmin + @PostMapping("/sync-oa-department") + @ApiOperationSupport(order = 9) + @Operation(summary = "同步OA部门") + public R syncOaDepartment( + @RequestParam(defaultValue = "1") Integer current, + @RequestParam(defaultValue = "50") Integer size) { + return R.data(oaSyncService.syncDepartmentPage(current, size)); + } + /** * 删除 */ @IsAdmin @PostMapping("/remove") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 10) @Operation(summary = "删除", description = "传入ids") public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { CacheUtil.clear(SYS_CACHE); @@ -188,7 +217,7 @@ public class DeptController extends BladeController { */ @PreAuth(AuthConstant.PERMIT_ALL) @GetMapping("/select") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 11) @Operation(summary = "下拉数据源", description = "传入id集合") public R> select(Long userId, String deptId) { if (Func.isNotEmpty(userId)) { @@ -205,7 +234,7 @@ public class DeptController extends BladeController { */ @PreAuth(AuthConstant.PERMIT_ALL) @GetMapping("/platform-company-select") - @ApiOperationSupport(order = 10) + @ApiOperationSupport(order = 12) @Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表") public R> platformCompanySelect() { return R.data(deptService.listPlatformCompany()); @@ -216,7 +245,7 @@ public class DeptController extends BladeController { */ @IsAdmin @GetMapping("/dept-leader-info") - @ApiOperationSupport(order = 11) + @ApiOperationSupport(order = 13) @Operation(summary = "获取部门的主管信息", description = "传入deptId") public R> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) { List list = deptService.deptLeaderInfo(deptId); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java index 45b7437..3e11ad4 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java @@ -1,5 +1,6 @@ package org.springblade.system.service; +import org.springblade.system.pojo.vo.OaOrgSyncPageVO; import org.springblade.system.pojo.vo.OaPersonSyncPageVO; /** @@ -36,4 +37,22 @@ public interface IOASyncService { * @return 本页同步结果 */ OaPersonSyncPageVO syncPersonFromUserList(int current, int size); + + /** + * 按页从 OA 公司接口同步公司 + * + * @param current 当前页,从 1 开始 + * @param size 每页条数 + * @return 本页同步结果 + */ + OaOrgSyncPageVO syncCompanyPage(int current, int size); + + /** + * 按页从 OA 部门接口同步部门;最后一页完成后更新祖级列表 + * + * @param current 当前页,从 1 开始 + * @param size 每页条数 + * @return 本页同步结果 + */ + OaOrgSyncPageVO syncDepartmentPage(int current, int size); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 0a02139..5e208c7 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -18,6 +18,7 @@ import org.springblade.system.log.ComposeLogUtil; import org.springblade.system.pojo.entity.*; import org.springblade.system.pojo.enums.DataSync; import org.springblade.system.pojo.enums.DeptCategory; +import org.springblade.system.pojo.vo.OaOrgSyncPageVO; import org.springblade.system.pojo.vo.OaPersonSyncPageVO; import org.springblade.system.service.*; import org.springblade.system.util.DataSyncRecordUtils; @@ -124,6 +125,28 @@ public class OASyncServiceImpl implements IOASyncService { } } + @Transactional(rollbackFor = Exception.class) + @Override + public OaOrgSyncPageVO syncCompanyPage(int current, int size) { + try { + ComposeLogUtil.addLog(log); + return this.syncCompanyFromOaPage(current, size); + } finally { + ComposeLogUtil.removeLastLog(); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public OaOrgSyncPageVO syncDepartmentPage(int current, int size) { + try { + ComposeLogUtil.addLog(log); + return this.syncDepartmentFromOaPage(current, size); + } finally { + ComposeLogUtil.removeLastLog(); + } + } + /** * 同步并记录 * @@ -166,20 +189,15 @@ public class OASyncServiceImpl implements IOASyncService { // 未处理的数据 List notHandleList = new ArrayList<>(); // 1. 设置查询参数 - OACompanySearch companySearch = new OACompanySearch(); - companySearch.setCurPage(1); - if (startTime != null) { - // 开始时间不为空,设置修改时间参数 - companySearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); - } + OACompanySearch companySearch = buildCompanySearch(startTime); // 2. 分页查询并处理数据 OAUtils.pageSyncHandler(companySearch, param -> oaClient.queryCompanyPage(new OASearch<>(param)), response -> { ComposeLogUtil.getLastLog().error("调用OA接口查询公司信息失败 {}", JSON.toJSONString(response)); return new ServiceException("调用OA接口查询公司信息失败"); }, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> { // 处理数据 - List deptList = handleCompany(list); - notHandleList.addAll(deptList); + OrgSyncCount syncCount = handleCompany(list); + notHandleList.addAll(syncCount.getNotHandledList()); }); // 3. 未处理的数据 if (CollectionUtil.isNotEmpty(notHandleList)) { @@ -203,21 +221,15 @@ public class OASyncServiceImpl implements IOASyncService { // 未处理的数据 List notHandleList = new ArrayList<>(); // 1. 设置查询参数 - OADepartmentSearch departmentSearch = new OADepartmentSearch(); - departmentSearch.setCurPage(1); - departmentSearch.setSubcompanyid1(subCompanyIds); - if (startTime != null) { - // 开始时间不为空,设置修改时间参数 - departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); - } + OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, subCompanyIds); // 2. 分页查询并处理数据 OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> { ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(response)); return new ServiceException("调用OA接口查询部门信息失败"); }, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> { // 处理数据 - List deptList = handleDept(list); - notHandleList.addAll(deptList); + OrgSyncCount syncCount = handleDept(list); + notHandleList.addAll(syncCount.getNotHandledList()); }); // 3. 未处理的数据 if (CollectionUtil.isNotEmpty(notHandleList)) { @@ -300,6 +312,139 @@ public class OASyncServiceImpl implements IOASyncService { return pageVO; } + /** + * 按页从 OA 公司接口同步公司 + * + * @param current 当前页 + * @param size 每页条数 + * @return 本页同步结果 + */ + private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size) { + int pageNo = current < 1 ? 1 : current; + int pageSize = size < 1 ? 50 : Math.min(size, 200); + OACompanySearch companySearch = buildCompanySearch(null); + companySearch.setCurPage(pageNo); + companySearch.setPageSize(pageSize); + OAResponse oaResponse = oaClient.queryCompanyPage(new OASearch<>(companySearch)); + if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) { + ComposeLogUtil.getLastLog().error("调用OA接口查询公司信息失败 {}", JSON.toJSONString(oaResponse)); + throw new ServiceException("调用OA接口查询公司信息失败"); + } + OAResponseData responseData = oaResponse.getData(); + List oaCompanies = responseData.getDataList() == null + ? Collections.emptyList() : responseData.getDataList(); + long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize(); + OrgSyncCount syncCount = handleCompany(oaCompanies); + if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) { + ComposeLogUtil.getLastLog().warn("同步公司,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList())); + } + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("company", pageNo, pageSize, totalSize, oaCompanies.size(), syncCount); + ComposeLogUtil.getLastLog().info("OA公司分页同步完成 {}/{},成功{},跳过{}", + pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount()); + return pageVO; + } + + /** + * 按页从 OA 部门接口同步部门 + * + * @param current 当前页 + * @param size 每页条数 + * @return 本页同步结果 + */ + private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size) { + int pageNo = current < 1 ? 1 : current; + int pageSize = size < 1 ? 50 : Math.min(size, 200); + String subCompanyIds = getSubCompanyIds(); + if (StringUtils.isEmpty(subCompanyIds)) { + ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数,跳过部门同步"); + OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty()); + emptyPageVO.setFinished(true); + return emptyPageVO; + } + OADepartmentSearch departmentSearch = buildDepartmentSearch(null, subCompanyIds); + departmentSearch.setCurPage(pageNo); + departmentSearch.setPageSize(pageSize); + OAResponse oaResponse = oaClient.queryDepartmentPage(new OASearch<>(departmentSearch)); + if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) { + ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(oaResponse)); + throw new ServiceException("调用OA接口查询部门信息失败"); + } + OAResponseData responseData = oaResponse.getData(); + List oaDepartments = responseData.getDataList() == null + ? Collections.emptyList() : responseData.getDataList(); + long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize(); + OrgSyncCount syncCount = handleDept(oaDepartments); + if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) { + ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList())); + } + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount); + if (Boolean.TRUE.equals(pageVO.getFinished())) { + // 部门同步完成后更新祖级列表 + deptService.updateAncestors(null); + } + ComposeLogUtil.getLastLog().info("OA部门分页同步完成 {}/{},成功{},跳过{}", + pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount()); + return pageVO; + } + + /** + * 组装组织分页同步结果 + */ + private OaOrgSyncPageVO buildOrgSyncPageVO(String stage, int pageNo, int pageSize, long totalSize, + int fetchedCount, OrgSyncCount syncCount) { + OaOrgSyncPageVO pageVO = new OaOrgSyncPageVO(); + pageVO.setStage(stage); + pageVO.setCurrent(pageNo); + pageVO.setSize(pageSize); + pageVO.setTotal(totalSize); + pageVO.setFetchedCount(fetchedCount); + pageVO.setSyncedCount(syncCount.getSyncedCount()); + pageVO.setSkippedCount(syncCount.getSkippedCount()); + boolean finished = fetchedCount == 0 + || fetchedCount < pageSize + || (long) pageNo * pageSize >= totalSize; + pageVO.setFinished(finished); + return pageVO; + } + + /** + * 组装 OA 公司分页查询参数 + * + * @param startTime 增量查询开始时间 + * @return 查询参数 + */ + private OACompanySearch buildCompanySearch(Date startTime) { + OACompanySearch companySearch = new OACompanySearch(); + companySearch.setCurPage(1); + companySearch.setPageSize(50); + if (startTime != null) { + companySearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); + } + return companySearch; + } + + /** + * 组装 OA 部门分页查询参数 + * + * @param startTime 增量查询开始时间 + * @param subCompanyIds 子公司 id 列表 + * @return 查询参数 + */ + private OADepartmentSearch buildDepartmentSearch(Date startTime, String subCompanyIds) { + OADepartmentSearch departmentSearch = new OADepartmentSearch(); + departmentSearch.setCurPage(1); + departmentSearch.setPageSize(50); + departmentSearch.setSubcompanyid1(subCompanyIds); + if (startTime != null) { + departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); + } + return departmentSearch; + } + /** * 组装 OA 人员分页查询参数 * @@ -325,12 +470,11 @@ public class OASyncServiceImpl implements IOASyncService { /** * 处理oa公司 * @param oaCompanies - * @return 未处理的数据 + * @return 同步统计 */ - private List handleCompany(List oaCompanies) { + private OrgSyncCount handleCompany(List oaCompanies) { if (CollectionUtil.isEmpty(oaCompanies)) { - // 数据为空,直接返回 - return Collections.emptyList(); + return OrgSyncCount.empty(); } // 获取需要的公司名称 Set companyNames = getCompanyNames(); @@ -340,30 +484,36 @@ public class OASyncServiceImpl implements IOASyncService { .filter(company -> companyNames.contains(company.getSubcompanyname())) .map(deptConvert::company2dept) .toList(); + int filteredSkipCount = oaCompanies.size() - allParam.size(); + if (CollectionUtil.isEmpty(allParam)) { + return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList()); + } // 查询数据库的部门,转换成map Map deptMap = getAllCompanyDeptMap(); // 处理部门 - return handleDept(allParam, deptMap, DeptCategory.COMPANY); + List notHandledList = handleDept(allParam, deptMap, DeptCategory.COMPANY); + int syncedCount = allParam.size() - notHandledList.size(); + int skippedCount = filteredSkipCount + notHandledList.size(); + return new OrgSyncCount(syncedCount, skippedCount, notHandledList); } /** * 处理oa部门 * @param oaDepts - * @return 未处理的数据 + * @return 同步统计 */ - private List handleDept(List oaDepts) { + private OrgSyncCount handleDept(List oaDepts) { if (CollectionUtil.isEmpty(oaDepts)) { - // 数据为空,直接返回 - return Collections.emptyList(); + return OrgSyncCount.empty(); } // 查询所有公司的编码和id的map Map companyDeptMap = getAllCompanyDeptMap(); // 根公司id String rootCompanyId = getRootCompanyId(); if (rootCompanyId == null) { - return Collections.emptyList(); + return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList()); } // 根公司下要同步的部门名称 Set rootCompanyDeptNames = getRootCompanyDeptNames(); @@ -373,8 +523,9 @@ public class OASyncServiceImpl implements IOASyncService { .filter(oaDept -> !rootCompanyId.equals(oaDept.getSubcompanyid1()) || (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname()))) .map(dept -> deptConvert.dept2dept(dept, companyDeptMap)) .toList(); + int filteredSkipCount = oaDepts.size() - allParam.size(); if (CollectionUtil.isEmpty(allParam)) { - return Collections.emptyList(); + return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList()); } Set deptCodes = allParam.stream() .map(Dept::getDeptCode) @@ -387,7 +538,10 @@ public class OASyncServiceImpl implements IOASyncService { .filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode())) .collect(Collectors.toMap(Dept::getDeptCode, Dept::getId, (a, b) -> b)); // 处理部门 - return this.handleDept(allParam, deptMap, DeptCategory.DEPT); + List notHandledList = this.handleDept(allParam, deptMap, DeptCategory.DEPT); + int syncedCount = allParam.size() - notHandledList.size(); + int skippedCount = filteredSkipCount + notHandledList.size(); + return new OrgSyncCount(syncedCount, skippedCount, notHandledList); } /** @@ -441,6 +595,37 @@ public class OASyncServiceImpl implements IOASyncService { .toList(); } + /** + * 组织同步统计 + */ + private static class OrgSyncCount { + private final int syncedCount; + private final int skippedCount; + private final List notHandledList; + + private OrgSyncCount(int syncedCount, int skippedCount, List notHandledList) { + this.syncedCount = syncedCount; + this.skippedCount = skippedCount; + this.notHandledList = notHandledList == null ? Collections.emptyList() : notHandledList; + } + + private static OrgSyncCount empty() { + return new OrgSyncCount(0, 0, Collections.emptyList()); + } + + private int getSyncedCount() { + return syncedCount; + } + + private int getSkippedCount() { + return skippedCount; + } + + private List getNotHandledList() { + return notHandledList; + } + } + /** * 获取oa查询参数,子公司id参数 * @return From a2e23ea264d3eaf363cd0561334806550c456f2e Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 18 Sep 2026 21:12:01 +0800 Subject: [PATCH 108/114] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E6=94=B9=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E7=BB=84=E7=BB=87=202=E3=80=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=8C=97=E6=96=97=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/vo/WaybillLocateVO.java | 74 +++++++ .../transport/pojo/vo/WaybillTrackVO.java | 95 +++++++++ .../src/main/resources/application.yml | 6 +- blade-service/blade-transport/pom.xml | 4 + .../controller/WaybillController.java | 19 ++ .../transport/service/IWaybillService.java | 20 ++ .../service/impl/WaybillServiceImpl.java | 200 +++++++++++++++++- .../src/main/resources/application-dev.yml | 7 + blade-third-party-api/blade-lbs-api/pom.xml | 22 ++ .../lbs/config/LbsFeignClientConfig.java | 69 ++++++ .../thirdparty/lbs/config/LbsProperties.java | 54 +++++ .../ThirdPartyLbsAutoConfiguration.java | 39 ++++ .../thirdparty/lbs/constant/LbsConstant.java | 52 +++++ .../thirdparty/lbs/feign/ILbsClient.java | 51 +++++ .../interceptor/LbsRequestInterceptor.java | 115 ++++++++++ .../lbs/pojo/dto/LbsLocateRequest.java | 75 +++++++ .../lbs/pojo/vo/LbsLocateResponse.java | 100 +++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../thirdparty/oa/feign/IOAClient.java | 4 +- blade-third-party-api/pom.xml | 1 + doc/nacos/blade-dev.yaml | 9 +- doc/nacos/blade-prod.yaml | 9 +- doc/nacos/third-party-api.yaml | 9 +- pom.xml | 5 + 24 files changed, 1031 insertions(+), 9 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillLocateVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillTrackVO.java create mode 100644 blade-third-party-api/blade-lbs-api/pom.xml create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsFeignClientConfig.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/ThirdPartyLbsAutoConfiguration.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/constant/LbsConstant.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/interceptor/LbsRequestInterceptor.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/dto/LbsLocateRequest.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java create mode 100644 blade-third-party-api/blade-lbs-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillLocateVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillLocateVO.java new file mode 100644 index 0000000..c3ce796 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillLocateVO.java @@ -0,0 +1,74 @@ +/** + * 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.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Map; + +/** + * 运单车辆实时定位结果 + * + * @author Chill + */ +@Data +@Schema(description = "运单车辆实时定位结果") +public class WaybillLocateVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "地址") + private String address; + + @Schema(description = "定位时间") + private String locateTime; + + @Schema(description = "速度") + private String speed; + + @Schema(description = "方向") + private String direction; + + @Schema(description = "LBS原始数据") + private Map rawData; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillTrackVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillTrackVO.java new file mode 100644 index 0000000..44a9e1b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillTrackVO.java @@ -0,0 +1,95 @@ +/** + * 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.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 运单历史轨迹结果 + * + * @author Chill + */ +@Data +@Schema(description = "运单历史轨迹结果") +public class WaybillTrackVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "开始日期") + private String startDate; + + @Schema(description = "结束日期") + private String endDate; + + @Schema(description = "轨迹点数量") + private Integer total; + + @Schema(description = "轨迹点列表") + private List points = new ArrayList<>(); + + @Data + @Schema(description = "轨迹点") + public static class WaybillTrackPointVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "定位时间") + private String locateTime; + + @Schema(description = "速度") + private String speed; + + @Schema(description = "方向") + private String direction; + + @Schema(description = "地址") + private String address; + } +} diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index 2d04e41..1447a57 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -33,7 +33,11 @@ iam: profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50} -# OA人员同步走同一 gwzh 网关,仅需 Authorization(与可用 curl 一致) +# OA组织/人员同步走同一 gwzh 网关,仅需 Authorization(与可用 curl 一致) thirdParty: oa: + baseUrl: ${OA_BASE_URL:http://172.16.204.83:38000} + queryCompanyPageUrl: ${OA_QUERY_COMPANY_PAGE_URL:/gwzh/OA/OA_GET_COMPANY_LIST} + queryDepartmentPageUrl: ${OA_QUERY_DEPARTMENT_PAGE_URL:/gwzh/OA/OA_GET_DEPARTMENT_LIST} + queryPersonPageUrl: ${OA_QUERY_PERSON_PAGE_URL:/gwzh/OA/OA_GET_USER_LIST} authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}} diff --git a/blade-service/blade-transport/pom.xml b/blade-service/blade-transport/pom.xml index 852de9a..b012436 100644 --- a/blade-service/blade-transport/pom.xml +++ b/blade-service/blade-transport/pom.xml @@ -31,6 +31,10 @@ org.springblade blade-transport-api + + org.springblade + blade-lbs-api + org.springblade blade-user-api diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index 8479ad4..ba2c241 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -53,6 +53,8 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.WaybillImportBatchVO; +import org.springblade.transport.pojo.vo.WaybillLocateVO; +import org.springblade.transport.pojo.vo.WaybillTrackVO; import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.pojo.dto.WaybillMileageRequest; @@ -109,6 +111,23 @@ public class WaybillController extends BladeController { return R.data(waybillService.listPunchRecords(waybillId)); } + @PostMapping("/locate") + @ApiOperationSupport(order = 1) + @Operation(summary = "车辆实时定位", description = "按运单绑定车牌调用 LBS_LOCATE") + public R locate(@Parameter(description = "运单ID", required = true) @RequestParam Long id) { + return R.data(waybillService.locateVehicle(id)); + } + + @PostMapping("/track") + @ApiOperationSupport(order = 1) + @Operation(summary = "车辆历史轨迹", description = "按运单绑定车牌 + 日期区间调用 LBS_LOCATE") + public R track( + @Parameter(description = "运单ID", required = true) @RequestParam Long id, + @Parameter(description = "开始日期 YYYY-MM-DD", required = true) @RequestParam String startDate, + @Parameter(description = "结束日期 YYYY-MM-DD", required = true) @RequestParam String endDate) { + return R.data(waybillService.trackVehicle(id, startDate, endDate)); + } + @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页", description = "传入waybill") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java index bf3dccf..3536790 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java @@ -28,6 +28,8 @@ import org.springblade.transport.excel.WaybillExcel; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; +import org.springblade.transport.pojo.vo.WaybillLocateVO; +import org.springblade.transport.pojo.vo.WaybillTrackVO; import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO; import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.pojo.dto.WaybillMileageRequest; @@ -49,6 +51,24 @@ public interface IWaybillService extends BaseService { */ WaybillPunchRecordsVO listPunchRecords(Long waybillId); + /** + * 运单车辆实时定位(按运单绑定车牌调用 LBS) + * + * @param id 运单ID + * @return 定位结果 + */ + WaybillLocateVO locateVehicle(Long id); + + /** + * 运单历史轨迹回放(按运单绑定车牌 + 日期区间调用 LBS) + * + * @param id 运单ID + * @param startDate 开始日期 YYYY-MM-DD + * @param endDate 结束日期 YYYY-MM-DD + * @return 轨迹结果 + */ + WaybillTrackVO trackVehicle(Long id, String startDate, String endDate); + Waybill syncDriverAcceptState(Waybill waybill); boolean submit(Waybill waybill); boolean saveDraft(Waybill waybill); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index b837dd8..b22ce13 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -25,6 +25,7 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.fasterxml.jackson.databind.JsonNode; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.jackson.JsonUtil; @@ -32,10 +33,10 @@ import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; +import org.springblade.thirdparty.lbs.feign.ILbsClient; +import org.springblade.thirdparty.lbs.pojo.dto.LbsLocateRequest; +import org.springblade.thirdparty.lbs.pojo.vo.LbsLocateResponse; import org.springblade.transport.excel.WaybillExcel; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; import org.springblade.transport.mapper.WaybillEnroutePunchMapper; import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.mapper.WaybillNodePunchMapper; @@ -49,6 +50,8 @@ import org.springblade.transport.pojo.entity.WaybillNodePunch; import org.springblade.transport.pojo.dto.WaybillMileageRequest; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingManageVO; +import org.springblade.transport.pojo.vo.WaybillLocateVO; +import org.springblade.transport.pojo.vo.WaybillTrackVO; import org.springblade.transport.pojo.vo.WaybillPunchPhotoVO; import org.springblade.transport.pojo.vo.WaybillPunchRecordItemVO; import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO; @@ -67,7 +70,11 @@ import org.springframework.transaction.annotation.Transactional; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; @@ -111,6 +118,9 @@ public class WaybillServiceImpl extends BaseServiceImpl @jakarta.annotation.Resource private WaybillEnroutePunchMapper waybillEnroutePunchMapper; + @jakarta.annotation.Resource + private ILbsClient lbsClient; + @Override public IPage selectWaybillPage(IPage page, WaybillVO waybill) { IPage entityPage = page(page, buildQuery(waybill)); @@ -128,6 +138,190 @@ public class WaybillServiceImpl extends BaseServiceImpl return result; } + @Override + public WaybillLocateVO locateVehicle(Long id) { + Waybill waybill = requireWaybillWithVehicleNo(id, "无法实时定位"); + String vehicleNo = waybill.getVehicleNo().trim(); + LbsLocateResponse response = invokeLbsLocate(id, vehicleNo, new LbsLocateRequest(vehicleNo), "实时定位"); + List> pointMaps = extractLbsPointMaps(response.getData()); + if (pointMaps.isEmpty()) { + throw new ServiceException("暂无车辆实时定位数据"); + } + return buildLocateVO(id, vehicleNo, pointMaps.get(0)); + } + + @Override + public WaybillTrackVO trackVehicle(Long id, String startDate, String endDate) { + Waybill waybill = requireWaybillWithVehicleNo(id, "无法查询历史轨迹"); + String vehicleNo = waybill.getVehicleNo().trim(); + String normalizedStart = normalizeTrackDate(startDate, "开始日期"); + String normalizedEnd = normalizeTrackDate(endDate, "结束日期"); + if (LocalDate.parse(normalizedStart).isAfter(LocalDate.parse(normalizedEnd))) { + throw new ServiceException("开始日期不能晚于结束日期"); + } + LbsLocateResponse response = invokeLbsLocate(id, vehicleNo, + new LbsLocateRequest(vehicleNo, normalizedStart, normalizedEnd), "历史轨迹"); + List> pointMaps = extractLbsPointMaps(response.getData()); + WaybillTrackVO trackVO = new WaybillTrackVO(); + trackVO.setWaybillId(id); + trackVO.setVehicleNo(vehicleNo); + trackVO.setStartDate(normalizedStart); + trackVO.setEndDate(normalizedEnd); + List points = new ArrayList<>(); + for (Map pointMap : pointMaps) { + WaybillTrackVO.WaybillTrackPointVO point = buildTrackPoint(vehicleNo, pointMap); + if (point.getLongitude() != null && point.getLatitude() != null) { + points.add(point); + } + } + trackVO.setPoints(points); + trackVO.setTotal(points.size()); + return trackVO; + } + + private Waybill requireWaybillWithVehicleNo(Long id, String actionTip) { + if (id == null) { + throw new ServiceException("运单ID不能为空"); + } + Waybill waybill = getById(id); + if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) { + throw new ServiceException("运单不存在"); + } + String vehicleNo = Func.toStr(waybill.getVehicleNo(), "").trim(); + if (Func.isEmpty(vehicleNo)) { + throw new ServiceException("运单未绑定车牌号," + actionTip); + } + waybill.setVehicleNo(vehicleNo); + return waybill; + } + + private LbsLocateResponse invokeLbsLocate(Long waybillId, String vehicleNo, LbsLocateRequest request, String scene) { + LbsLocateResponse response; + try { + response = lbsClient.locate(request); + } catch (Exception exception) { + log.error("调用LBS{}失败 waybillId={}, vehicleNo={}", scene, waybillId, vehicleNo, exception); + throw new ServiceException("调用车辆" + scene + "接口失败"); + } + if (response == null || !response.isSuccess()) { + String errorMessage = response == null ? "车辆" + scene + "无返回" : response.errorMessage(); + throw new ServiceException(errorMessage); + } + return response; + } + + private String normalizeTrackDate(String dateText, String fieldName) { + if (Func.isEmpty(dateText)) { + throw new ServiceException(fieldName + "不能为空"); + } + String normalized = dateText.trim(); + try { + return LocalDate.parse(normalized, DateTimeFormatter.ISO_LOCAL_DATE).toString(); + } catch (DateTimeParseException exception) { + throw new ServiceException(fieldName + "格式必须为YYYY-MM-DD"); + } + } + + /** + * 提取 LBS 轨迹点列表,兼容 data 数组 或 data.list 数组 + */ + private List> extractLbsPointMaps(JsonNode dataNode) { + List> pointMaps = new ArrayList<>(); + if (dataNode == null || dataNode.isNull()) { + return pointMaps; + } + JsonNode listNode = dataNode; + if (dataNode.isObject() && dataNode.has("list")) { + listNode = dataNode.get("list"); + } + if (listNode != null && listNode.isArray()) { + for (JsonNode item : listNode) { + if (item != null && item.isObject()) { + pointMaps.add(JsonUtil.toMap(item.toString())); + } + } + return pointMaps; + } + if (dataNode.isObject()) { + pointMaps.add(JsonUtil.toMap(dataNode.toString())); + } + return pointMaps; + } + + /** + * 将 LBS 单点数据归一化为实时定位结果 + */ + private WaybillLocateVO buildLocateVO(Long waybillId, String vehicleNo, Map pointMap) { + WaybillLocateVO locateVO = new WaybillLocateVO(); + locateVO.setWaybillId(waybillId); + locateVO.setVehicleNo(vehicleNo); + locateVO.setRawData(pointMap); + fillPointFields(locateVO, pointMap); + return locateVO; + } + + private WaybillTrackVO.WaybillTrackPointVO buildTrackPoint(String vehicleNo, Map pointMap) { + WaybillTrackVO.WaybillTrackPointVO point = new WaybillTrackVO.WaybillTrackPointVO(); + point.setVehicleNo(vehicleNo); + fillPointFields(point, pointMap); + return point; + } + + private void fillPointFields(WaybillLocateVO target, Map pointMap) { + target.setLongitude(firstDecimal(pointMap, "longitude", "lon", "lng", "x", "jd", "Longitude", "LON", "LNG", "JD", "X")); + target.setLatitude(firstDecimal(pointMap, "latitude", "lat", "y", "wd", "Latitude", "LAT", "WD", "Y")); + target.setAddress(firstText(pointMap, "address", "addr", "adr", "wz", "Address", "ADDR", "ADR")); + target.setLocateTime(firstText(pointMap, "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time")); + target.setSpeed(firstText(pointMap, "speed", "v", "sd", "Speed", "SD", "V")); + target.setDirection(firstText(pointMap, "direct", "direction", "h", "fx", "Direction", "FX", "course", "H")); + String responseVehicleNo = firstText(pointMap, "cph", "vehicleNo", "plateNo", "CPH"); + if (Func.isNotEmpty(responseVehicleNo)) { + target.setVehicleNo(responseVehicleNo); + } + } + + private void fillPointFields(WaybillTrackVO.WaybillTrackPointVO target, Map pointMap) { + target.setLongitude(firstDecimal(pointMap, "longitude", "lon", "lng", "x", "jd", "Longitude", "LON", "LNG", "JD", "X")); + target.setLatitude(firstDecimal(pointMap, "latitude", "lat", "y", "wd", "Latitude", "LAT", "WD", "Y")); + target.setAddress(firstText(pointMap, "address", "addr", "adr", "wz", "Address", "ADDR", "ADR")); + target.setLocateTime(firstText(pointMap, "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time")); + target.setSpeed(firstText(pointMap, "speed", "v", "sd", "Speed", "SD", "V")); + target.setDirection(firstText(pointMap, "direct", "direction", "h", "fx", "Direction", "FX", "course", "H")); + String responseVehicleNo = firstText(pointMap, "cph", "vehicleNo", "plateNo", "CPH"); + if (Func.isNotEmpty(responseVehicleNo)) { + target.setVehicleNo(responseVehicleNo); + } + } + + private BigDecimal firstDecimal(Map rawData, String... keys) { + String text = firstText(rawData, keys); + if (Func.isEmpty(text)) { + return null; + } + try { + return new BigDecimal(text.trim()); + } catch (NumberFormatException exception) { + return null; + } + } + + private String firstText(Map rawData, String... keys) { + if (rawData == null || rawData.isEmpty() || keys == null) { + return null; + } + for (String key : keys) { + Object value = rawData.get(key); + if (value == null) { + continue; + } + String text = String.valueOf(value).trim(); + if (Func.isNotEmpty(text) && !"null".equalsIgnoreCase(text)) { + return text; + } + } + return null; + } + @Override public WaybillPunchRecordsVO listPunchRecords(Long waybillId) { WaybillPunchRecordsVO vo = new WaybillPunchRecordsVO(); diff --git a/blade-service/blade-transport/src/main/resources/application-dev.yml b/blade-service/blade-transport/src/main/resources/application-dev.yml index 048e650..29ae464 100644 --- a/blade-service/blade-transport/src/main/resources/application-dev.yml +++ b/blade-service/blade-transport/src/main/resources/application-dev.yml @@ -8,3 +8,10 @@ spring: url: ${blade.datasource.dev.url} username: ${blade.datasource.dev.username} password: ${blade.datasource.dev.password} + +# LBS 车辆实时定位(Authorization 与 OA 人员接口一致) +thirdParty: + lbs: + baseUrl: ${LBS_BASE_URL:http://172.16.204.83:38000} + locateUrl: ${LBS_LOCATE_URL:/gwzh/LBS/LBS_LOCATE} + authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}} diff --git a/blade-third-party-api/blade-lbs-api/pom.xml b/blade-third-party-api/blade-lbs-api/pom.xml new file mode 100644 index 0000000..fe6d45d --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + org.springblade + blade-third-party-api + ${revision} + + + blade-lbs-api + ${project.artifactId} + jar + + + + org.springblade + blade-core-tool + + + diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsFeignClientConfig.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsFeignClientConfig.java new file mode 100644 index 0000000..bd85839 --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsFeignClientConfig.java @@ -0,0 +1,69 @@ +/** + * 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.thirdparty.lbs.config; + +import feign.Logger; +import feign.Request; +import org.springblade.thirdparty.lbs.interceptor.LbsRequestInterceptor; +import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; +import org.springframework.context.annotation.Bean; + +import java.util.concurrent.TimeUnit; + +/** + * LBS Feign 客户端配置。 + *

+ * 关闭父上下文继承,避免全局 BladeFeignRequestInterceptor 透传登录态请求头导致 gwzh 400。 + * + * @author Chill + */ +public class LbsFeignClientConfig { + + @Bean + public FeignClientConfigurer feignClientConfigurer() { + return new FeignClientConfigurer() { + @Override + public boolean inheritParentConfiguration() { + return false; + } + }; + } + + @Bean + public LbsRequestInterceptor requestInterceptor(LbsProperties lbsProperties) { + return new LbsRequestInterceptor(lbsProperties); + } + + @Bean + public Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } + + @Bean + public Request.Options options() { + return new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true); + } +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java new file mode 100644 index 0000000..0e8b780 --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java @@ -0,0 +1,54 @@ +/** + * 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.thirdparty.lbs.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * LBS 配置 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "third-party.lbs") +public class LbsProperties { + + /** + * LBS 网关基础地址 + */ + private String baseUrl; + + /** + * 车辆实时定位路径 + */ + private String locateUrl = "/gwzh/LBS/LBS_LOCATE"; + + /** + * gwzh 网关 Authorization(Basic),与 OA 人员接口一致 + */ + private String authorization; +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/ThirdPartyLbsAutoConfiguration.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/ThirdPartyLbsAutoConfiguration.java new file mode 100644 index 0000000..8550141 --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/ThirdPartyLbsAutoConfiguration.java @@ -0,0 +1,39 @@ +/** + * 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.thirdparty.lbs.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 第三方 LBS 自动配置 + * + * @author Chill + */ +@EnableConfigurationProperties(LbsProperties.class) +@Configuration +public class ThirdPartyLbsAutoConfiguration { +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/constant/LbsConstant.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/constant/LbsConstant.java new file mode 100644 index 0000000..8f4a009 --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/constant/LbsConstant.java @@ -0,0 +1,52 @@ +/** + * 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.thirdparty.lbs.constant; + +/** + * LBS 常量 + * + * @author Chill + */ +public final class LbsConstant { + + private LbsConstant() { + } + + /** + * 成功响应码(文档标注) + */ + public static final int SUCCESS_STATUS = 200; + + /** + * 成功响应码(字符串) + */ + public static final String SUCCESS_STATUS_TEXT = "200"; + + /** + * gwzh 常见成功码(与 OA 一致) + */ + public static final String SUCCESS_CODE = "1"; +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java new file mode 100644 index 0000000..e19a148 --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java @@ -0,0 +1,51 @@ +/** + * 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.thirdparty.lbs.feign; + +import org.springblade.thirdparty.lbs.config.LbsFeignClientConfig; +import org.springblade.thirdparty.lbs.pojo.dto.LbsLocateRequest; +import org.springblade.thirdparty.lbs.pojo.vo.LbsLocateResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * LBS 接口 + * + * @author Chill + */ +@FeignClient(name = "LBS", url = "${thirdParty.lbs.baseUrl}", configuration = LbsFeignClientConfig.class) +public interface ILbsClient { + + /** + * 车辆实时定位 + * + * @param request 请求(cph=车牌号) + * @return 定位结果 + */ + @PostMapping("${thirdParty.lbs.locateUrl:/gwzh/LBS/LBS_LOCATE}") + LbsLocateResponse locate(@RequestBody LbsLocateRequest request); +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/interceptor/LbsRequestInterceptor.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/interceptor/LbsRequestInterceptor.java new file mode 100644 index 0000000..d458cd9 --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/interceptor/LbsRequestInterceptor.java @@ -0,0 +1,115 @@ +/** + * 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.thirdparty.lbs.interceptor; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.thirdparty.lbs.config.LbsProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * LBS Feign 请求拦截器:仅发送 Authorization + Content-Type,与 OA 人员接口一致。 + * + * @author Chill + */ +@Slf4j +@RequiredArgsConstructor +public class LbsRequestInterceptor implements RequestInterceptor { + + private static final String BASIC_PREFIX = "Basic "; + private static final String BEARER_PREFIX = "Bearer "; + private static final Set KEEP_HEADERS = Set.of( + HttpHeaders.AUTHORIZATION.toLowerCase(Locale.ROOT), + HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT) + ); + + private final LbsProperties lbsProperties; + + @Override + public void apply(RequestTemplate template) { + stripUnwantedHeaders(template); + template.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + + String authorization = normalizeAuthorization(lbsProperties.getAuthorization()); + if (StringUtil.isNotBlank(authorization)) { + template.header(HttpHeaders.AUTHORIZATION, authorization); + } else { + log.warn("LBS Feign 未配置 third-party.lbs.authorization,gwzh 网关可能拒绝请求"); + } + + logRequest(template); + } + + private void logRequest(RequestTemplate template) { + String bodyText = ""; + byte[] body = template.body(); + if (body != null && body.length > 0) { + Charset charset = template.requestCharset() == null ? StandardCharsets.UTF_8 : template.requestCharset(); + bodyText = new String(body, charset); + } + log.info("LBS Feign 请求 method={}, url={}{}{}, headers={}, body={}", + template.method(), + template.feignTarget() == null ? "" : template.feignTarget().url(), + template.path(), + template.queryLine() == null ? "" : template.queryLine(), + template.headers(), + bodyText); + } + + private void stripUnwantedHeaders(RequestTemplate template) { + Map> headers = template.headers(); + List headerNames = new ArrayList<>(headers.keySet()); + for (String headerName : headerNames) { + if (!KEEP_HEADERS.contains(headerName.toLowerCase(Locale.ROOT))) { + template.removeHeader(headerName); + } + } + } + + private String normalizeAuthorization(String authorization) { + if (StringUtil.isBlank(authorization)) { + return authorization; + } + if (StringUtil.startsWithIgnoreCase(authorization, BASIC_PREFIX) + || StringUtil.startsWithIgnoreCase(authorization, BEARER_PREFIX)) { + return authorization; + } + return BASIC_PREFIX + authorization; + } +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/dto/LbsLocateRequest.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/dto/LbsLocateRequest.java new file mode 100644 index 0000000..4c20460 --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/dto/LbsLocateRequest.java @@ -0,0 +1,75 @@ +/** + * 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.thirdparty.lbs.pojo.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +/** + * LBS 定位/历史轨迹请求 + * + * @author Chill + */ +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class LbsLocateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 车牌号 + */ + private String cph; + + /** + * 开始日期,格式 YYYY-MM-DD(历史轨迹) + */ + @JsonProperty("start_date") + private String startDate; + + /** + * 结束日期,格式 YYYY-MM-DD(历史轨迹) + */ + @JsonProperty("end_date") + private String endDate; + + public LbsLocateRequest(String cph) { + this.cph = cph; + } + + public LbsLocateRequest(String cph, String startDate, String endDate) { + this.cph = cph; + this.startDate = startDate; + this.endDate = endDate; + } +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java new file mode 100644 index 0000000..6432a2b --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java @@ -0,0 +1,100 @@ +/** + * 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.thirdparty.lbs.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.springblade.thirdparty.lbs.constant.LbsConstant; + +import java.io.Serial; +import java.io.Serializable; + +/** + * LBS 定位/历史轨迹响应 + * + * @author Chill + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class LbsLocateResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 响应代码,200 为正确 + */ + private Integer status; + + /** + * 兼容 code 字段(网关常返回 "1") + */ + private String code; + + /** + * 提示信息 + */ + private String msg; + + /** + * 提示信息(兼容 message) + */ + private String message; + + /** + * 定位/轨迹数据(数组,或 {list,total}) + */ + private JsonNode data; + + /** + * 是否成功 + */ + public boolean isSuccess() { + if (status != null && (status == LbsConstant.SUCCESS_STATUS || status == 1)) { + return true; + } + if (code == null) { + return false; + } + String normalized = code.trim(); + return LbsConstant.SUCCESS_STATUS_TEXT.equals(normalized) + || LbsConstant.SUCCESS_CODE.equals(normalized); + } + + /** + * 错误信息 + */ + public String errorMessage() { + if (msg != null && !msg.isBlank()) { + return msg; + } + if (message != null && !message.isBlank()) { + return message; + } + return "LBS接口调用失败"; + } +} diff --git a/blade-third-party-api/blade-lbs-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/blade-third-party-api/blade-lbs-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..85802bf --- /dev/null +++ b/blade-third-party-api/blade-lbs-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springblade.thirdparty.lbs.config.ThirdPartyLbsAutoConfiguration diff --git a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java index 9b36edd..63233c5 100644 --- a/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java +++ b/blade-third-party-api/blade-oa-api/src/main/java/org/springblade/thirdparty/oa/feign/IOAClient.java @@ -26,7 +26,7 @@ public interface IOAClient { * @param param * @return */ - @PostMapping("${thirdParty.oa.queryCompanyPageUrl:/api/hrm/resful/getHrmsubcompanyWithPage}") + @PostMapping("${thirdParty.oa.queryCompanyPageUrl:/gwzh/OA/OA_GET_COMPANY_LIST}") OAResponse queryCompanyPage(@RequestBody OASearch param); /** @@ -34,7 +34,7 @@ public interface IOAClient { * @param param * @return */ - @PostMapping("${thirdParty.oa.queryDepartmentPage:/api/hrm/resful/getHrmdepartmentWithPage}") + @PostMapping("${thirdParty.oa.queryDepartmentPageUrl:/gwzh/OA/OA_GET_DEPARTMENT_LIST}") OAResponse queryDepartmentPage(@RequestBody OASearch param); /** diff --git a/blade-third-party-api/pom.xml b/blade-third-party-api/pom.xml index ba27dd9..5e8c455 100644 --- a/blade-third-party-api/pom.xml +++ b/blade-third-party-api/pom.xml @@ -13,6 +13,7 @@ ${project.artifactId} blade-oa-api + blade-lbs-api blade-mk-api blade-wps-api blade-ocr-api diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index a2763a4..0ff37d3 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -86,9 +86,16 @@ thirdParty: baseUrl: http://127.0.0.1:8080 oa: # OA开放接口地址 - baseUrl: http://127.0.0.1:8080 + baseUrl: http://172.16.204.83:38000 + queryCompanyPageUrl: /gwzh/OA/OA_GET_COMPANY_LIST + queryDepartmentPageUrl: /gwzh/OA/OA_GET_DEPARTMENT_LIST queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}} + lbs: + # LBS 网关地址(车辆实时定位) + baseUrl: http://172.16.204.83:38000 + locateUrl: /gwzh/LBS/LBS_LOCATE + authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}} track: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 7279339..cfc872f 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -58,9 +58,16 @@ thirdParty: baseUrl: http://127.0.0.1:8080 oa: # OA开放接口地址 - baseUrl: http://127.0.0.1:8080 + baseUrl: http://172.16.204.83:38000 + queryCompanyPageUrl: /gwzh/OA/OA_GET_COMPANY_LIST + queryDepartmentPageUrl: /gwzh/OA/OA_GET_DEPARTMENT_LIST queryPersonPageUrl: /gwzh/OA/OA_GET_USER_LIST authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}} + lbs: + # LBS 网关地址(车辆实时定位) + baseUrl: http://172.16.204.83:38000 + locateUrl: /gwzh/LBS/LBS_LOCATE + authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}} track: # 轨迹开放接口地址 baseUrl: http://127.0.0.1:8080 diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index baa53a0..e71be46 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -10,9 +10,16 @@ thirdParty: baseUrl: ${WPS_BASE_URL:http://127.0.0.1:8080} oa: # OA开放接口地址 - baseUrl: ${OA_BASE_URL:http://127.0.0.1:8080} + baseUrl: ${OA_BASE_URL:http://172.16.204.83:38000} + queryCompanyPageUrl: ${OA_QUERY_COMPANY_PAGE_URL:/gwzh/OA/OA_GET_COMPANY_LIST} + queryDepartmentPageUrl: ${OA_QUERY_DEPARTMENT_PAGE_URL:/gwzh/OA/OA_GET_DEPARTMENT_LIST} queryPersonPageUrl: ${OA_QUERY_PERSON_PAGE_URL:/gwzh/OA/OA_GET_USER_LIST} authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:}} + lbs: + # LBS 网关地址(车辆实时定位) + baseUrl: ${LBS_BASE_URL:${OA_BASE_URL:http://172.16.204.83:38000}} + locateUrl: ${LBS_LOCATE_URL:/gwzh/LBS/LBS_LOCATE} + authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:}}} track: # 轨迹开放接口地址 baseUrl: ${TRACK_BASE_URL:http://127.0.0.1:8080} diff --git a/pom.xml b/pom.xml index ae708e0..c4d7d9b 100644 --- a/pom.xml +++ b/pom.xml @@ -124,6 +124,11 @@ blade-oa-api ${revision} + + org.springblade + blade-lbs-api + ${revision} + org.springblade blade-open-api From 380fd117c5fd91524f6bf1740da96d3be9df1919 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 18 Sep 2026 22:12:43 +0800 Subject: [PATCH 109/114] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/controller/DeptController.java | 4 +- .../service/impl/OASyncServiceImpl.java | 8 +- .../controller/WaybillController.java | 2 +- .../service/impl/WaybillServiceImpl.java | 77 +++++++++++++------ .../src/main/resources/application-dev.yml | 1 + .../thirdparty/lbs/config/LbsProperties.java | 5 ++ .../thirdparty/lbs/feign/ILbsClient.java | 9 +++ .../lbs/pojo/vo/LbsLocateResponse.java | 24 ++++-- doc/nacos/blade-dev.yaml | 1 + doc/nacos/blade-prod.yaml | 1 + doc/nacos/third-party-api.yaml | 1 + 11 files changed, 96 insertions(+), 37 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index e407597..b2f2280 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -182,7 +182,7 @@ public class DeptController extends BladeController { @Operation(summary = "同步OA公司") public R syncOaCompany( @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "50") Integer size) { + @RequestParam(defaultValue = "20") Integer size) { return R.data(oaSyncService.syncCompanyPage(current, size)); } @@ -195,7 +195,7 @@ public class DeptController extends BladeController { @Operation(summary = "同步OA部门") public R syncOaDepartment( @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "50") Integer size) { + @RequestParam(defaultValue = "20") Integer size) { return R.data(oaSyncService.syncDepartmentPage(current, size)); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 5e208c7..0792dc2 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -321,7 +321,7 @@ public class OASyncServiceImpl implements IOASyncService { */ private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size) { int pageNo = current < 1 ? 1 : current; - int pageSize = size < 1 ? 50 : Math.min(size, 200); + int pageSize = size < 1 ? 20 : Math.min(size, 200); OACompanySearch companySearch = buildCompanySearch(null); companySearch.setCurPage(pageNo); companySearch.setPageSize(pageSize); @@ -355,7 +355,7 @@ public class OASyncServiceImpl implements IOASyncService { */ private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size) { int pageNo = current < 1 ? 1 : current; - int pageSize = size < 1 ? 50 : Math.min(size, 200); + int pageSize = size < 1 ? 20 : Math.min(size, 200); String subCompanyIds = getSubCompanyIds(); if (StringUtils.isEmpty(subCompanyIds)) { ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数,跳过部门同步"); @@ -420,7 +420,7 @@ public class OASyncServiceImpl implements IOASyncService { private OACompanySearch buildCompanySearch(Date startTime) { OACompanySearch companySearch = new OACompanySearch(); companySearch.setCurPage(1); - companySearch.setPageSize(50); + companySearch.setPageSize(20); if (startTime != null) { companySearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); } @@ -437,7 +437,7 @@ public class OASyncServiceImpl implements IOASyncService { private OADepartmentSearch buildDepartmentSearch(Date startTime, String subCompanyIds) { OADepartmentSearch departmentSearch = new OADepartmentSearch(); departmentSearch.setCurPage(1); - departmentSearch.setPageSize(50); + departmentSearch.setPageSize(20); departmentSearch.setSubcompanyid1(subCompanyIds); if (startTime != null) { departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index ba2c241..68b83b1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -120,7 +120,7 @@ public class WaybillController extends BladeController { @PostMapping("/track") @ApiOperationSupport(order = 1) - @Operation(summary = "车辆历史轨迹", description = "按运单绑定车牌 + 日期区间调用 LBS_LOCATE") + @Operation(summary = "车辆历史轨迹", description = "按运单绑定车牌 + 日期区间调用 LBS_TRACK") public R track( @Parameter(description = "运单ID", required = true) @RequestParam Long id, @Parameter(description = "开始日期 YYYY-MM-DD", required = true) @RequestParam String startDate, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index b22ce13..42d9932 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -142,8 +142,8 @@ public class WaybillServiceImpl extends BaseServiceImpl public WaybillLocateVO locateVehicle(Long id) { Waybill waybill = requireWaybillWithVehicleNo(id, "无法实时定位"); String vehicleNo = waybill.getVehicleNo().trim(); - LbsLocateResponse response = invokeLbsLocate(id, vehicleNo, new LbsLocateRequest(vehicleNo), "实时定位"); - List> pointMaps = extractLbsPointMaps(response.getData()); + LbsLocateResponse response = invokeLbs(id, vehicleNo, new LbsLocateRequest(vehicleNo), false, "实时定位"); + List> pointMaps = extractLbsPointMaps(response); if (pointMaps.isEmpty()) { throw new ServiceException("暂无车辆实时定位数据"); } @@ -159,9 +159,9 @@ public class WaybillServiceImpl extends BaseServiceImpl if (LocalDate.parse(normalizedStart).isAfter(LocalDate.parse(normalizedEnd))) { throw new ServiceException("开始日期不能晚于结束日期"); } - LbsLocateResponse response = invokeLbsLocate(id, vehicleNo, - new LbsLocateRequest(vehicleNo, normalizedStart, normalizedEnd), "历史轨迹"); - List> pointMaps = extractLbsPointMaps(response.getData()); + LbsLocateResponse response = invokeLbs(id, vehicleNo, + new LbsLocateRequest(vehicleNo, normalizedStart, normalizedEnd), true, "历史轨迹"); + List> pointMaps = extractLbsPointMaps(response); WaybillTrackVO trackVO = new WaybillTrackVO(); trackVO.setWaybillId(id); trackVO.setVehicleNo(vehicleNo); @@ -195,10 +195,11 @@ public class WaybillServiceImpl extends BaseServiceImpl return waybill; } - private LbsLocateResponse invokeLbsLocate(Long waybillId, String vehicleNo, LbsLocateRequest request, String scene) { + private LbsLocateResponse invokeLbs(Long waybillId, String vehicleNo, LbsLocateRequest request, + boolean trackMode, String scene) { LbsLocateResponse response; try { - response = lbsClient.locate(request); + response = trackMode ? lbsClient.track(request) : lbsClient.locate(request); } catch (Exception exception) { log.error("调用LBS{}失败 waybillId={}, vehicleNo={}", scene, waybillId, vehicleNo, exception); throw new ServiceException("调用车辆" + scene + "接口失败"); @@ -223,29 +224,55 @@ public class WaybillServiceImpl extends BaseServiceImpl } /** - * 提取 LBS 轨迹点列表,兼容 data 数组 或 data.list 数组 + * 提取 LBS 轨迹点:优先 list,其次 obj,再兼容 data / data.list / data.obj */ - private List> extractLbsPointMaps(JsonNode dataNode) { + private List> extractLbsPointMaps(LbsLocateResponse response) { List> pointMaps = new ArrayList<>(); - if (dataNode == null || dataNode.isNull()) { + if (response == null) { return pointMaps; } - JsonNode listNode = dataNode; - if (dataNode.isObject() && dataNode.has("list")) { - listNode = dataNode.get("list"); + appendLbsNodes(pointMaps, response.getList()); + if (!pointMaps.isEmpty()) { + return pointMaps; } - if (listNode != null && listNode.isArray()) { - for (JsonNode item : listNode) { + appendLbsNodes(pointMaps, response.getObj()); + if (!pointMaps.isEmpty()) { + return pointMaps; + } + JsonNode dataNode = response.getData(); + if (dataNode != null && !dataNode.isNull()) { + if (dataNode.isObject() && dataNode.has("list")) { + appendLbsNodes(pointMaps, dataNode.get("list")); + if (!pointMaps.isEmpty()) { + return pointMaps; + } + } + if (dataNode.isObject() && dataNode.has("obj")) { + appendLbsNodes(pointMaps, dataNode.get("obj")); + if (!pointMaps.isEmpty()) { + return pointMaps; + } + } + appendLbsNodes(pointMaps, dataNode); + } + return pointMaps; + } + + private void appendLbsNodes(List> pointMaps, JsonNode node) { + if (node == null || node.isNull()) { + return; + } + if (node.isArray()) { + for (JsonNode item : node) { if (item != null && item.isObject()) { pointMaps.add(JsonUtil.toMap(item.toString())); } } - return pointMaps; + return; } - if (dataNode.isObject()) { - pointMaps.add(JsonUtil.toMap(dataNode.toString())); + if (node.isObject()) { + pointMaps.add(JsonUtil.toMap(node.toString())); } - return pointMaps; } /** @@ -271,10 +298,10 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setLongitude(firstDecimal(pointMap, "longitude", "lon", "lng", "x", "jd", "Longitude", "LON", "LNG", "JD", "X")); target.setLatitude(firstDecimal(pointMap, "latitude", "lat", "y", "wd", "Latitude", "LAT", "WD", "Y")); target.setAddress(firstText(pointMap, "address", "addr", "adr", "wz", "Address", "ADDR", "ADR")); - target.setLocateTime(firstText(pointMap, "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time")); - target.setSpeed(firstText(pointMap, "speed", "v", "sd", "Speed", "SD", "V")); + target.setLocateTime(firstText(pointMap, "utc", "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time")); + target.setSpeed(firstText(pointMap, "spd", "speed", "v", "sd", "Speed", "SD", "V")); target.setDirection(firstText(pointMap, "direct", "direction", "h", "fx", "Direction", "FX", "course", "H")); - String responseVehicleNo = firstText(pointMap, "cph", "vehicleNo", "plateNo", "CPH"); + String responseVehicleNo = firstText(pointMap, "vno", "cph", "vehicleNo", "plateNo", "CPH", "VNO"); if (Func.isNotEmpty(responseVehicleNo)) { target.setVehicleNo(responseVehicleNo); } @@ -284,10 +311,10 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setLongitude(firstDecimal(pointMap, "longitude", "lon", "lng", "x", "jd", "Longitude", "LON", "LNG", "JD", "X")); target.setLatitude(firstDecimal(pointMap, "latitude", "lat", "y", "wd", "Latitude", "LAT", "WD", "Y")); target.setAddress(firstText(pointMap, "address", "addr", "adr", "wz", "Address", "ADDR", "ADR")); - target.setLocateTime(firstText(pointMap, "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time")); - target.setSpeed(firstText(pointMap, "speed", "v", "sd", "Speed", "SD", "V")); + target.setLocateTime(firstText(pointMap, "utc", "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time")); + target.setSpeed(firstText(pointMap, "spd", "speed", "v", "sd", "Speed", "SD", "V")); target.setDirection(firstText(pointMap, "direct", "direction", "h", "fx", "Direction", "FX", "course", "H")); - String responseVehicleNo = firstText(pointMap, "cph", "vehicleNo", "plateNo", "CPH"); + String responseVehicleNo = firstText(pointMap, "vno", "cph", "vehicleNo", "plateNo", "CPH", "VNO"); if (Func.isNotEmpty(responseVehicleNo)) { target.setVehicleNo(responseVehicleNo); } diff --git a/blade-service/blade-transport/src/main/resources/application-dev.yml b/blade-service/blade-transport/src/main/resources/application-dev.yml index 29ae464..ebffbff 100644 --- a/blade-service/blade-transport/src/main/resources/application-dev.yml +++ b/blade-service/blade-transport/src/main/resources/application-dev.yml @@ -14,4 +14,5 @@ thirdParty: lbs: baseUrl: ${LBS_BASE_URL:http://172.16.204.83:38000} locateUrl: ${LBS_LOCATE_URL:/gwzh/LBS/LBS_LOCATE} + trackUrl: ${LBS_TRACK_URL:/gwzh/LBS/LBS_TRACK} authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}} diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java index 0e8b780..5b4f355 100644 --- a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/config/LbsProperties.java @@ -47,6 +47,11 @@ public class LbsProperties { */ private String locateUrl = "/gwzh/LBS/LBS_LOCATE"; + /** + * 车辆历史轨迹路径 + */ + private String trackUrl = "/gwzh/LBS/LBS_TRACK"; + /** * gwzh 网关 Authorization(Basic),与 OA 人员接口一致 */ diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java index e19a148..7db03f2 100644 --- a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/feign/ILbsClient.java @@ -48,4 +48,13 @@ public interface ILbsClient { */ @PostMapping("${thirdParty.lbs.locateUrl:/gwzh/LBS/LBS_LOCATE}") LbsLocateResponse locate(@RequestBody LbsLocateRequest request); + + /** + * 车辆历史轨迹 + * + * @param request 请求(cph、start_date、end_date) + * @return 轨迹结果 + */ + @PostMapping("${thirdParty.lbs.trackUrl:/gwzh/LBS/LBS_TRACK}") + LbsLocateResponse track(@RequestBody LbsLocateRequest request); } diff --git a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java index 6432a2b..be72bc6 100644 --- a/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java +++ b/blade-third-party-api/blade-lbs-api/src/main/java/org/springblade/thirdparty/lbs/pojo/vo/LbsLocateResponse.java @@ -34,7 +34,10 @@ import java.io.Serial; import java.io.Serializable; /** - * LBS 定位/历史轨迹响应 + * LBS 定位/历史轨迹响应。 + *

+ * 实际网关返回示例: + * {@code {"code":200,"obj":{...},"list":null,"msg":"OK"}} * * @author Chill */ @@ -51,7 +54,7 @@ public class LbsLocateResponse implements Serializable { private Integer status; /** - * 兼容 code 字段(网关常返回 "1") + * 响应代码(网关返回 200 或 "1") */ private String code; @@ -66,7 +69,17 @@ public class LbsLocateResponse implements Serializable { private String message; /** - * 定位/轨迹数据(数组,或 {list,total}) + * 单点定位对象 + */ + private JsonNode obj; + + /** + * 历史轨迹点列表 + */ + private JsonNode list; + + /** + * 兼容旧字段 data */ private JsonNode data; @@ -89,10 +102,11 @@ public class LbsLocateResponse implements Serializable { * 错误信息 */ public String errorMessage() { - if (msg != null && !msg.isBlank()) { + if (msg != null && !msg.isBlank() && !"OK".equalsIgnoreCase(msg) && !"Success".equalsIgnoreCase(msg)) { return msg; } - if (message != null && !message.isBlank()) { + if (message != null && !message.isBlank() + && !"OK".equalsIgnoreCase(message) && !"Success".equalsIgnoreCase(message)) { return message; } return "LBS接口调用失败"; diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index 0ff37d3..d6b0821 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -95,6 +95,7 @@ thirdParty: # LBS 网关地址(车辆实时定位) baseUrl: http://172.16.204.83:38000 locateUrl: /gwzh/LBS/LBS_LOCATE + trackUrl: /gwzh/LBS/LBS_TRACK authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}} track: # 轨迹开放接口地址 diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index cfc872f..407b09c 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -67,6 +67,7 @@ thirdParty: # LBS 网关地址(车辆实时定位) baseUrl: http://172.16.204.83:38000 locateUrl: /gwzh/LBS/LBS_LOCATE + trackUrl: /gwzh/LBS/LBS_TRACK authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}} track: # 轨迹开放接口地址 diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index e71be46..11e17b3 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -19,6 +19,7 @@ thirdParty: # LBS 网关地址(车辆实时定位) baseUrl: ${LBS_BASE_URL:${OA_BASE_URL:http://172.16.204.83:38000}} locateUrl: ${LBS_LOCATE_URL:/gwzh/LBS/LBS_LOCATE} + trackUrl: ${LBS_TRACK_URL:/gwzh/LBS/LBS_TRACK} authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:}}} track: # 轨迹开放接口地址 From a138f3b916d0825d7a66b8fda62afd21430babf4 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 20 Sep 2026 13:16:44 +0800 Subject: [PATCH 110/114] =?UTF-8?q?=E8=B0=83=E8=AF=95mk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/provider/AuthProvider.java | 2 + .../system/pojo/entity/MeasurementUnit.java | 6 ++ .../controller/BusinessProcessController.java | 16 +++- .../service/IBusinessProcessService.java | 9 +++ .../impl/BusinessProcessServiceImpl.java | 73 +++++++++++++++++ .../system/mapper/MeasurementUnitMapper.xml | 6 ++ .../impl/MeasurementUnitServiceImpl.java | 21 +++++ .../CustomerArchivePublicController.java | 81 +++++++++++++++++++ .../service/ICustomerArchiveService.java | 17 ++++ .../impl/CustomerArchiveServiceImpl.java | 74 ++++++++++++----- doc/nacos/blade.yaml | 2 + doc/sql/bladex/bladex.mysql.all.create.sql | 2 + doc/sql/transport/blade_measurement_unit.sql | 2 + .../blade_measurement_unit_code_20260919.sql | 11 +++ 14 files changed, 296 insertions(+), 26 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java create mode 100644 doc/sql/transport/blade_measurement_unit_code_20260919.sql 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 a259f7b..c67671b 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 @@ -63,6 +63,8 @@ public class AuthProvider { DEFAULT_SKIP_URL.add("/manager/check-upload"); DEFAULT_SKIP_URL.add("/assets/**"); DEFAULT_SKIP_URL.add("/iam/sso/token/**"); + DEFAULT_SKIP_URL.add("/blade-transport/customer-archive/public/**"); + DEFAULT_SKIP_URL.add("/customer-archive/public/**"); } /** diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java index ad80a1e..7cdde90 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java @@ -43,6 +43,12 @@ public class MeasurementUnit extends BaseEntity { @Serial private static final long serialVersionUID = 1L; + /** + * 计量单位编码 + */ + @Schema(description = "计量单位编码") + private String unitCode; + /** * 计量单位 */ 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 index c976569..8241b21 100644 --- 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 @@ -17,6 +17,7 @@ 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.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -52,29 +53,36 @@ public class BusinessProcessController extends BladeController { return R.data(pages); } - @GetMapping("/isEditView") + @PostMapping("/processSubmit") @ApiOperationSupport(order = 2) + @Operation(summary = "提交MK审核流", description = "调用mk processSubmit,传入templateCode/submitIdentity/formInstanceId") + public R processSubmit(@Validated @RequestBody MKProcessCreateDTO param) { + return R.data(businessProcessService.processSubmit(param)); + } + + @GetMapping("/isEditView") + @ApiOperationSupport(order = 3) @Operation(summary = "是否编辑页", description = "传入业务id") public R isEditView(@Valid @NotBlank(message = "业务id不能为空") String bizId) { return R.data(businessProcessService.isEditView(bizId)); } @GetMapping("/getMKApprovalUrl") - @ApiOperationSupport(order = 3) + @ApiOperationSupport(order = 4) @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) + @ApiOperationSupport(order = 5) @Operation(summary = "查询审批记录", description = "传入业务id或流程实例id") public R> getApprovedRecords(String bizId, String processInstanceId) { return R.data(businessProcessService.queryApprovedRecords(bizId, processInstanceId)); } @GetMapping("/downloadFile") - @ApiOperationSupport(order = 5) + @ApiOperationSupport(order = 6) @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/service/IBusinessProcessService.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java index 5e3e2ff..a577c09 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 @@ -6,6 +6,7 @@ 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 org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO; import java.util.List; @@ -25,6 +26,14 @@ public interface IBusinessProcessService extends IService { */ BusinessProcessVO submitBusinessProcess(BusinessProcessSubmitDTO param); + /** + * 直接调用 MK processSubmit 提交流程 + * + * @param param MK 流程创建参数 + * @return 流程实例 id + */ + String processSubmit(MKProcessCreateDTO param); + /** * 修改业务流程状态 * @param 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 50984d3..1317fdf 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 @@ -24,6 +24,8 @@ 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.system.pojo.entity.User; +import org.springblade.system.service.IUserService; import org.springblade.thirdparty.mk.config.MKProperties; import org.springblade.thirdparty.mk.constant.MKConstant; import org.springblade.thirdparty.mk.constant.MKDoc; @@ -61,6 +63,7 @@ public class BusinessProcessServiceImpl extends ServiceImpllambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + ); + if (businessProcess == null) { + businessProcess = new BusinessProcess(); + businessProcess.setBizId(bizId); + businessProcess.setProcessType(param.getTemplateCode()); + businessProcess.setSubject(param.getSubject()); + businessProcess.setPromoterId(AuthUtil.getUserId()); + businessProcess.setPromoterName(AuthUtil.getNickName()); + businessProcess.setPromoterLoginName(loginName); + businessProcess.setSubmitTime(new Date()); + } else { + businessProcess.setProcessType(param.getTemplateCode()); + if (StringUtil.isNotBlank(param.getSubject())) { + businessProcess.setSubject(param.getSubject()); + } + businessProcess.setPromoterLoginName(loginName); + if (businessProcess.getSubmitTime() == null) { + businessProcess.setSubmitTime(new Date()); + } + } + businessProcess.setProcessInstanceId(processInstanceId); + businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue()); + this.saveOrUpdate(businessProcess); + return processInstanceId; + } + + /** + * 从当前登录用户实体读取真实手机号(绕过接口返回脱敏) + */ + private String resolveCurrentUserPhone() { + Long userId = AuthUtil.getUserId(); + if (userId != null) { + User user = userService.getById(userId); + if (user != null && StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) { + return user.getPhone().trim(); + } + if (user != null && StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) { + return user.getAccount().trim(); + } + } + String account = AuthUtil.getUserAccount(); + if (StringUtil.isNotBlank(account) && account.matches("^1\\d{10}$")) { + return account.trim(); + } + return null; + } + @Transactional(rollbackFor = Exception.class) @Override public String updateBusinessProcessStatus(BusinessProcessUpdateDTO param) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml index 387f1a2..10fbb90 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml @@ -13,6 +13,7 @@ + @@ -30,6 +31,7 @@ mmu.update_time, mmu.status, mmu.is_deleted, + mmu.unit_code, mmu.unit_name, mmu.dimension, mmu.remark @@ -39,6 +41,10 @@ LEFT JOIN blade_user uu ON uu.id = mmu.update_user WHERE mmu.is_deleted = 0 + + + AND mmu.unit_code LIKE #{unitCodeLike} + AND mmu.unit_name LIKE #{unitNameLike} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java index 615bb67..df18f4f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java @@ -46,6 +46,7 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl UNIT_CODE_MAX_LENGTH) { + throw new ServiceException("计量单位编码不能超过50字"); + } if (Func.isEmpty(measurementUnit.getUnitName())) { throw new ServiceException("计量单位不能为空"); } @@ -115,9 +123,22 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl queryWrapper = Wrappers.lambdaQuery() + .eq(MeasurementUnit::getUnitCode, measurementUnit.getUnitCode()) + .eq(MeasurementUnit::getIsDeleted, 0); + if (Func.isNotEmpty(measurementUnit.getId())) { + queryWrapper.ne(MeasurementUnit::getId, measurementUnit.getId()); + } + if (count(queryWrapper) > 0L) { + throw new ServiceException("该计量单位编码已存在"); + } + } + private void validateUniqueUnitName(MeasurementUnit measurementUnit) { LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() .eq(MeasurementUnit::getUnitName, measurementUnit.getUnitName()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java new file mode 100644 index 0000000..b2094c3 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java @@ -0,0 +1,81 @@ +/** + * 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. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.secure.constant.AuthConstant; +import org.springblade.core.tenant.annotation.TenantIgnore; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.vo.CustomerArchiveVO; +import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; +import org.springblade.transport.service.ICustomerArchiveService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 客商档案公开查看 控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@TenantIgnore +@PreAuth(AuthConstant.PERMIT_ALL) +@RequestMapping("/customer-archive/public") +@Tag(name = "客商档案公开查看", description = "客商档案公开查看") +public class CustomerArchivePublicController { + + private final ICustomerArchiveService customerArchiveService; + + /** + * 公开详情 + */ + @GetMapping("/detail") + @ApiOperationSupport(order = 1) + @Operation(summary = "公开详情", description = "传入id,无需登录") + public R detail(@Parameter(description = "主键", required = true) @RequestParam Long id) { + return R.data(customerArchiveService.publicDetail(id)); + } + + /** + * 公开变更记录分页 + */ + @GetMapping("/change-record/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "公开变更记录分页", description = "传入客商ID,无需登录") + public R> changeRecordList( + @Parameter(description = "客商ID", required = true) @RequestParam Long customerId, Query query) { + return R.data(customerArchiveService.publicChangeRecordPage(Condition.getPage(query), customerId)); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java index 73ddd1a..9a6472b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java @@ -66,6 +66,23 @@ public interface ICustomerArchiveService extends BaseService { */ CustomerArchiveVO detail(Long id); + /** + * 公开查看详情(不校验登录态与数据权限) + * + * @param id 主键 + * @return 客商档案详情 + */ + CustomerArchiveVO publicDetail(Long id); + + /** + * 公开查看变更记录分页(不校验登录态与数据权限) + * + * @param page 分页参数 + * @param customerId 客商ID + * @return 变更记录分页 + */ + IPage publicChangeRecordPage(IPage page, Long customerId); + /** * 新增或修改客商档案 *

仅当 {@code customer.recordChange = true}(前端点「提交」)时写入变更记录;「保存」不落变更记录。

diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index 7dde6cd..8485807 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -32,6 +32,7 @@ import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tenant.annotation.TenantIgnore; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; @@ -151,31 +152,28 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl recordPage = changeRecordMapper.selectPage( - new Page<>(page.getCurrent(), page.getSize()), - Wrappers.lambdaQuery() - .eq(CustomerChangeRecord::getCustomerId, customerId) - .eq(CustomerChangeRecord::getIsDeleted, 0) - .orderByDesc(CustomerChangeRecord::getChangeTime)); - page.setTotal(recordPage.getTotal()); - return page.setRecords(recordPage.getRecords().stream() - .map(record -> Objects.requireNonNull(BeanUtil.copyProperties(record, CustomerChangeRecordVO.class))) - .toList()); + return queryChangeRecordPage(page, customerId); + } + + @Override + @TenantIgnore + public IPage publicChangeRecordPage(IPage page, Long customerId) { + if (Func.isEmpty(customerId)) { + throw new ServiceException("客商ID不能为空"); + } + getExistingCustomer(customerId); + return queryChangeRecordPage(page, customerId); } @Override public CustomerArchiveVO detail(Long id) { - if (Func.isEmpty(id)) { - throw new ServiceException("主键不能为空"); - } - CustomerArchive customer = ensureCustomerAccessible(id); - CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class)); - detail.setContacts(loadContacts(id)); - detail.setReceiptAccounts(loadReceiptAccounts(id)); - detail.setInvoices(loadInvoices(id)); - detail.setScores(loadScores(id)); - fillFundUseRisk(List.of(detail)); - return detail; + return buildDetail(ensureCustomerAccessible(id)); + } + + @Override + @TenantIgnore + public CustomerArchiveVO publicDetail(Long id) { + return buildDetail(getExistingCustomer(id)); } @Override @@ -987,11 +985,43 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl queryChangeRecordPage(IPage page, Long customerId) { + IPage recordPage = changeRecordMapper.selectPage( + new Page<>(page.getCurrent(), page.getSize()), + Wrappers.lambdaQuery() + .eq(CustomerChangeRecord::getCustomerId, customerId) + .eq(CustomerChangeRecord::getIsDeleted, 0) + .orderByDesc(CustomerChangeRecord::getChangeTime)); + page.setTotal(recordPage.getTotal()); + return page.setRecords(recordPage.getRecords().stream() + .map(record -> Objects.requireNonNull(BeanUtil.copyProperties(record, CustomerChangeRecordVO.class))) + .toList()); + } + + private CustomerArchiveVO buildDetail(CustomerArchive customer) { + CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class)); + Long id = customer.getId(); + detail.setContacts(loadContacts(id)); + detail.setReceiptAccounts(loadReceiptAccounts(id)); + detail.setInvoices(loadInvoices(id)); + detail.setScores(loadScores(id)); + fillFundUseRisk(List.of(detail)); + return detail; + } + + private CustomerArchive getExistingCustomer(Long id) { + if (Func.isEmpty(id)) { + throw new ServiceException("主键不能为空"); + } CustomerArchive customer = getById(id); if (Func.isEmpty(customer) || Objects.equals(customer.getIsDeleted(), 1)) { throw new ServiceException("客商档案不存在"); } + return customer; + } + + private CustomerArchive ensureCustomerAccessible(Long id) { + CustomerArchive customer = getExistingCustomer(id); if (baseMapper.selectCustomerPermissionCount(id, AuthUtil.getUserId()) == 0) { throw new ServiceException("无权访问该客商档案"); } diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml index f65e82d..cc51556 100644 --- a/doc/nacos/blade.yaml +++ b/doc/nacos/blade.yaml @@ -216,6 +216,8 @@ blade: # 退出登录:允许无令牌/令牌失效时也能调用(服务端对无用户直接返回成功) - /oauth/logout/** - /blade-auth/oauth/logout/** + - /blade-transport/customer-archive/public/** + - /customer-archive/public/** #授权认证配置 auth: - method: ALL diff --git a/doc/sql/bladex/bladex.mysql.all.create.sql b/doc/sql/bladex/bladex.mysql.all.create.sql index f43daca..b805412 100644 --- a/doc/sql/bladex/bladex.mysql.all.create.sql +++ b/doc/sql/bladex/bladex.mysql.all.create.sql @@ -1757,6 +1757,7 @@ INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `s DROP TABLE IF EXISTS `blade_measurement_unit`; CREATE TABLE `blade_measurement_unit` ( `id` bigint NOT NULL COMMENT '主键', + `unit_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量单位编码', `unit_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量单位', `dimension` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量维度', `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', @@ -1768,6 +1769,7 @@ CREATE TABLE `blade_measurement_unit` ( `status` int NULL DEFAULT 1 COMMENT '状态', `is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除', PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_measurement_unit_code`(`unit_code`) USING BTREE, UNIQUE INDEX `uk_measurement_unit_name`(`unit_name`) USING BTREE, INDEX `idx_measurement_unit_dimension`(`dimension`) USING BTREE, INDEX `idx_measurement_unit_status`(`status`) USING BTREE diff --git a/doc/sql/transport/blade_measurement_unit.sql b/doc/sql/transport/blade_measurement_unit.sql index 8f03d43..175be76 100644 --- a/doc/sql/transport/blade_measurement_unit.sql +++ b/doc/sql/transport/blade_measurement_unit.sql @@ -4,6 +4,7 @@ DROP TABLE IF EXISTS `blade_measurement_unit`; CREATE TABLE `blade_measurement_unit` ( `id` bigint NOT NULL COMMENT '主键', + `unit_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量单位编码', `unit_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量单位', `dimension` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量维度', `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', @@ -15,6 +16,7 @@ CREATE TABLE `blade_measurement_unit` ( `status` int NULL DEFAULT 1 COMMENT '状态', `is_deleted` int NULL DEFAULT 0 COMMENT '是否已删除', PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_measurement_unit_code`(`unit_code`) USING BTREE, UNIQUE INDEX `uk_measurement_unit_name`(`unit_name`) USING BTREE, INDEX `idx_measurement_unit_dimension`(`dimension`) USING BTREE, INDEX `idx_measurement_unit_status`(`status`) USING BTREE diff --git a/doc/sql/transport/blade_measurement_unit_code_20260919.sql b/doc/sql/transport/blade_measurement_unit_code_20260919.sql new file mode 100644 index 0000000..5da38d2 --- /dev/null +++ b/doc/sql/transport/blade_measurement_unit_code_20260919.sql @@ -0,0 +1,11 @@ +-- 计量单位新增计量单位编码 +ALTER TABLE `blade_measurement_unit` + ADD COLUMN `unit_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '计量单位编码' AFTER `id`; + +UPDATE `blade_measurement_unit` +SET `unit_code` = CONCAT('MU', `id`) +WHERE `unit_code` IS NULL OR `unit_code` = ''; + +ALTER TABLE `blade_measurement_unit` + MODIFY COLUMN `unit_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计量单位编码', + ADD UNIQUE INDEX `uk_measurement_unit_code`(`unit_code`) USING BTREE; From aa2084090a05f4928aae015d87ac7cdb67dd901b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 20 Sep 2026 14:14:39 +0800 Subject: [PATCH 111/114] =?UTF-8?q?=E8=B0=83=E8=AF=95mk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/springblade/openapi/OpenApiApplication.java | 4 ++++ .../blade-openapi/src/main/resources/application.yml | 1 + .../blade-openapi/src/main/resources/openapi-lock.yaml | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 blade-service/blade-openapi/src/main/resources/openapi-lock.yaml 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 e76743f..9be7947 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 @@ -44,6 +44,10 @@ public class OpenApiApplication { public static void main(String[] args) { BladeApplication.disableNacosLaunchConfig(); + // 当前处理人刷新依赖 RedisLockClient;Nacos 全局 blade.lock.enabled=false 时仍需为本服务开启 + if (System.getProperty("blade.lock.enabled") == null) { + System.setProperty("blade.lock.enabled", "true"); + } BladeApplication.run(AppConstant.APPLICATION_OPENAPI_NAME, OpenApiApplication.class, args); } diff --git a/blade-service/blade-openapi/src/main/resources/application.yml b/blade-service/blade-openapi/src/main/resources/application.yml index be5d93f..2868632 100644 --- a/blade-service/blade-openapi/src/main/resources/application.yml +++ b/blade-service/blade-openapi/src/main/resources/application.yml @@ -10,6 +10,7 @@ spring: - nacos:blade-${spring.profiles.active}.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 + - optional:classpath:openapi-lock.yaml cloud: nacos: username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}} diff --git a/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml b/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml new file mode 100644 index 0000000..c67d709 --- /dev/null +++ b/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml @@ -0,0 +1,5 @@ +# openapi 当前处理人刷新依赖 Redisson 分布式锁。 +# 该文件必须在 nacos blade-*.yaml 之后导入,用于覆盖全局 blade.lock.enabled=false。 +blade: + lock: + enabled: true From d5e7787e89eb07ded8a7e7b20d6a9181c1c8c127 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 20 Sep 2026 16:05:25 +0800 Subject: [PATCH 112/114] =?UTF-8?q?=E8=B0=83=E8=AF=95mk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/openapi-lock.yaml | 4 ++++ .../CustomerArchivePublicController.java | 18 ++++++++++++++++++ doc/nacos/blade-prod.yaml | 1 + 3 files changed, 23 insertions(+) diff --git a/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml b/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml index c67d709..e388d4f 100644 --- a/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml +++ b/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml @@ -1,5 +1,9 @@ # openapi 当前处理人刷新依赖 Redisson 分布式锁。 # 该文件必须在 nacos blade-*.yaml 之后导入,用于覆盖全局 blade.lock.enabled=false。 +# 连接信息与业务 Redis 保持一致,避免 Redisson 因缺少密码出现 NOAUTH。 blade: lock: enabled: true + address: redis://${spring.data.redis.host:127.0.0.1}:${spring.data.redis.port:6379} + password: ${spring.data.redis.password:} + database: ${spring.data.redis.database:0} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java index b2094c3..8db8987 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java @@ -22,12 +22,14 @@ */ package org.springblade.transport.controller; +import com.alibaba.fastjson2.JSON; 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.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; @@ -38,15 +40,20 @@ import org.springblade.transport.pojo.vo.CustomerArchiveVO; import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; import org.springblade.transport.service.ICustomerArchiveService; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.Map; + /** * 客商档案公开查看 控制器 * * @author Chill */ +@Slf4j @RestController @AllArgsConstructor @TenantIgnore @@ -78,4 +85,15 @@ public class CustomerArchivePublicController { return R.data(customerArchiveService.publicChangeRecordPage(Condition.getPage(query), customerId)); } + /** + * 公开接收流程页 postMessage 数据(当前仅打印,便于联调) + */ + @PostMapping("/process-message") + @ApiOperationSupport(order = 3) + @Operation(summary = "公开接收流程消息", description = "无需登录,当前仅打印接收数据") + public R processMessage(@RequestBody Map body) { + log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body)); + return R.success("ok"); + } + } diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 407b09c..384f904 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -39,6 +39,7 @@ blade: ##将docker脚本部署的redis服务映射为宿主机ip ##生产环境推荐使用阿里云高可用redis服务并设置密码 address: redis://172.16.203.228:6379 + password: ${spring.data.redis.password:} #通用开发生产环境数据库地址(特殊情况可在对应的子工程里配置覆盖) datasource: prod: From 0bfc9cbc86b8ce76d51b6d5240e97d2d812ed808 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 20 Sep 2026 16:36:31 +0800 Subject: [PATCH 113/114] =?UTF-8?q?=E8=B0=83=E8=AF=95mk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/springblade/gateway/provider/AuthProvider.java | 2 ++ .../src/main/java/org/springblade/openapi/mk/Api4MK.java | 3 +++ doc/nacos/blade.yaml | 2 ++ 3 files changed, 7 insertions(+) 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 c67671b..c324c64 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 @@ -65,6 +65,8 @@ public class AuthProvider { DEFAULT_SKIP_URL.add("/iam/sso/token/**"); DEFAULT_SKIP_URL.add("/blade-transport/customer-archive/public/**"); DEFAULT_SKIP_URL.add("/customer-archive/public/**"); + DEFAULT_SKIP_URL.add("/blade-openapi/openApi/mk/process/commonCallback"); + DEFAULT_SKIP_URL.add("/openApi/mk/process/commonCallback"); } /** 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 index 0f43c46..727fce2 100644 --- 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 @@ -5,6 +5,8 @@ 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.secure.annotation.PreAuth; +import org.springblade.core.secure.constant.AuthConstant; import org.springblade.core.tool.api.FR; import org.springblade.openapi.mk.api.IApi4MK; import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO; @@ -49,6 +51,7 @@ public class Api4MK implements IApi4MK { } @Override + @PreAuth(AuthConstant.PERMIT_ALL) public FR processCommonCallback(Api4MKProcessApprovalDTO param) { log.info("mk流程通用回调 操作名称:{} 参数:{}", ProcessOperationType.getOperationName(param.getOperation()), JSON.toJSONString(param)); callback(param, ProcessHandler::approve); diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml index cc51556..6c40037 100644 --- a/doc/nacos/blade.yaml +++ b/doc/nacos/blade.yaml @@ -218,6 +218,8 @@ blade: - /blade-auth/oauth/logout/** - /blade-transport/customer-archive/public/** - /customer-archive/public/** + - /blade-openapi/openApi/mk/process/commonCallback + - /openApi/mk/process/commonCallback #授权认证配置 auth: - method: ALL From 6987a0e790c691fe0a54ccb328884fd67ff4a767 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 20 Sep 2026 17:11:08 +0800 Subject: [PATCH 114/114] =?UTF-8?q?=E8=B0=83=E8=AF=95mk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process/feign/IBusinessProcessClient.java | 12 ++++ .../process/feign/BusinessProcessClient.java | 12 ++++ .../service/IBusinessProcessService.java | 9 +++ .../impl/BusinessProcessServiceImpl.java | 27 +++++++++ blade-service/blade-transport/pom.xml | 4 ++ .../CustomerArchivePublicController.java | 55 ++++++++++++++++++- doc/nacos/blade.yaml | 1 + 7 files changed, 119 insertions(+), 1 deletion(-) 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 16cc18e..d6e5fa3 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 @@ -38,6 +38,7 @@ public interface IBusinessProcessClient { String QUERY_TODO_LIST = API_PREFIX + "/queryTodoList"; String QUERY_BUSINESS_PROCESS_SNAPSHOT = API_PREFIX + "/queryBusinessProcessSnapshot"; String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments"; + String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes"; /** * 提交业务流程 @@ -102,4 +103,15 @@ public interface IBusinessProcessClient { */ @GetMapping(QUERY_APPROVED_RECORD_LIST) FR> queryApprovedRecordsNoAttachments(@RequestParam(name = "bizId", required = false) String bizId, @RequestParam(name = "processInstanceId", required = false) String processInstanceId); + + /** + * 获取流程当前节点详情 + * + * @param processInstanceId 流程实例id + * @param loginName MK登录名(手机号) + * @return 当前节点详情 + */ + @GetMapping(GET_CURRENT_NODES) + FR getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam(value = "loginName", required = false) String loginName); } 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 eb789f4..b0f8b8e 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 @@ -3,6 +3,8 @@ package org.springblade.process.feign; import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.Valid; import lombok.AllArgsConstructor; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.secure.constant.AuthConstant; import org.springblade.core.tool.api.FR; import org.springblade.process.pojo.dto.BusinessProcessCurrentHandlerRefreshDTO; import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO; @@ -13,8 +15,10 @@ 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.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -74,4 +78,12 @@ public class BusinessProcessClient implements IBusinessProcessClient { public FR> queryApprovedRecordsNoAttachments(String bizId, String processInstanceId) { return FR.data(businessProcessService.queryApprovedRecordsNoAttachments(bizId, processInstanceId)); } + + @PreAuth(AuthConstant.PERMIT_ALL) + @GetMapping(GET_CURRENT_NODES) + @Override + public FR getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam(value = "loginName", required = false) String loginName) { + return FR.data(businessProcessService.getCurrentNodes(processInstanceId, loginName)); + } } 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 a577c09..ebdc83b 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 @@ -34,6 +34,15 @@ public interface IBusinessProcessService extends IService { */ String processSubmit(MKProcessCreateDTO param); + /** + * 获取流程当前节点详情 + * + * @param processInstanceId 流程实例id + * @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析 + * @return 当前节点列表 + */ + List getCurrentNodes(String processInstanceId, String loginName); + /** * 修改业务流程状态 * @param 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 1317fdf..4ad3724 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 @@ -163,9 +163,36 @@ public class BusinessProcessServiceImpl extends ServiceImpl getCurrentNodes(String processInstanceId, String loginName) { + if (StringUtils.isBlank(processInstanceId)) { + log.warn("查询当前节点失败,processInstanceId为空"); + return Collections.emptyList(); + } + String resolvedLoginName = loginName; + if (StringUtils.isBlank(resolvedLoginName)) { + BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId); + resolvedLoginName = resolvePromoterLoginName(businessProcess, null); + } + if (StringUtils.isBlank(resolvedLoginName)) { + log.warn("查询当前节点失败,loginName为空 processInstanceId={}", processInstanceId); + return Collections.emptyList(); + } + try { + List currentNodes = mkService.getCurrentNodes(processInstanceId, resolvedLoginName); + log.info("获取流程当前节点详情 processInstanceId={} loginName={} result={}", + processInstanceId, resolvedLoginName, JSON.toJSONString(currentNodes)); + return currentNodes == null ? Collections.emptyList() : currentNodes; + } catch (Exception e) { + log.error("查询当前节点异常 processInstanceId={} loginName={}", processInstanceId, resolvedLoginName, e); + return Collections.emptyList(); + } + } + /** * 从当前登录用户实体读取真实手机号(绕过接口返回脱敏) */ diff --git a/blade-service/blade-transport/pom.xml b/blade-service/blade-transport/pom.xml index b012436..f2490ca 100644 --- a/blade-service/blade-transport/pom.xml +++ b/blade-service/blade-transport/pom.xml @@ -43,6 +43,10 @@ org.springblade blade-system-api + + org.springblade + blade-process-api + org.springframework.boot spring-boot-starter-amqp diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java index 8db8987..3bbb385 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchivePublicController.java @@ -35,7 +35,10 @@ import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.secure.constant.AuthConstant; import org.springblade.core.tenant.annotation.TenantIgnore; +import org.springblade.core.tool.api.FR; import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.process.feign.IBusinessProcessClient; import org.springblade.transport.pojo.vo.CustomerArchiveVO; import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; import org.springblade.transport.service.ICustomerArchiveService; @@ -46,6 +49,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; /** @@ -63,6 +68,7 @@ import java.util.Map; public class CustomerArchivePublicController { private final ICustomerArchiveService customerArchiveService; + private final IBusinessProcessClient businessProcessClient; /** * 公开详情 @@ -90,10 +96,57 @@ public class CustomerArchivePublicController { */ @PostMapping("/process-message") @ApiOperationSupport(order = 3) - @Operation(summary = "公开接收流程消息", description = "无需登录,当前仅打印接收数据") + @Operation(summary = "公开接收流程消息", description = "无需登录,接收后查询当前节点并打印") public R processMessage(@RequestBody Map body) { log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body)); + Map formValues = asMap(body == null ? null : body.get("formValues")); + String processId = firstText(formValues, "processId"); + if (StringUtil.isBlank(processId) && body != null) { + processId = firstText(body, "processId"); + } + String loginName = firstText(formValues, "mkLoginName", "loginName"); + if (StringUtil.isBlank(processId)) { + log.warn("客商公开页流程消息未找到 processId,跳过查询当前节点"); + return R.success("ok"); + } + try { + FR result = businessProcessClient.getCurrentNodes(processId, loginName); + log.info("客商公开页流程消息当前节点详情 processId={} loginName={} result={}", + processId, loginName, JSON.toJSONString(result == null ? null : result.getData())); + } catch (Exception e) { + log.error("客商公开页查询当前节点失败 processId={} loginName={}", processId, loginName, e); + } return R.success("ok"); } + private static Map asMap(Object value) { + if (!(value instanceof Map map)) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(); + map.forEach((key, nested) -> { + if (key != null) { + result.put(String.valueOf(key), nested); + } + }); + return result; + } + + private static String firstText(Map source, String... keys) { + if (source == null || keys == null) { + return null; + } + for (String key : keys) { + Object value = source.get(key); + if (value == null) { + continue; + } + String text = String.valueOf(value).trim(); + if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) { + return text; + } + } + return null; + } + } diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml index 6c40037..052f97e 100644 --- a/doc/nacos/blade.yaml +++ b/doc/nacos/blade.yaml @@ -220,6 +220,7 @@ blade: - /customer-archive/public/** - /blade-openapi/openApi/mk/process/commonCallback - /openApi/mk/process/commonCallback + - /feign/client/businessProcess/getCurrentNodes #授权认证配置 auth: - method: ALL