Compare commits
29 Commits
6987a0e790
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 14c1d9dff0 | |||
| 5254a46de8 | |||
| 12ef1210bc | |||
| d1821bbfcf | |||
| 2fbbf9b0bb | |||
| 427e4b0c84 | |||
| 2404281bff | |||
| 2dafd44c26 | |||
| cfe36dfc8b | |||
| 6b97685165 | |||
| 0797785176 | |||
| fd6563f53b | |||
| ddfa52a2ac | |||
| 37d0ebc21e | |||
| 993f802111 | |||
| 4c96583701 | |||
| 8485efe00d | |||
| b02aff8ff6 | |||
| 37309b24f9 | |||
| 56c7bc020a | |||
| af1430eaee | |||
| 86adb47392 | |||
| 780cd56ffe | |||
| 8cf9140f56 | |||
| 304061774e | |||
| 98697af6df | |||
| 2f56ca07cb | |||
| 9668f6059a | |||
| cc33bc5ef2 |
@@ -25,6 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.springblade.auth.config;
|
package org.springblade.auth.config;
|
||||||
|
|
||||||
|
import org.springblade.auth.filter.LogoutLogFilter;
|
||||||
import org.springblade.auth.granter.IamAwareTokenGranterFactory;
|
import org.springblade.auth.granter.IamAwareTokenGranterFactory;
|
||||||
import org.springblade.auth.handler.BladeAuthorizationHandler;
|
import org.springblade.auth.handler.BladeAuthorizationHandler;
|
||||||
import org.springblade.auth.handler.BladeLockHandler;
|
import org.springblade.auth.handler.BladeLockHandler;
|
||||||
@@ -53,9 +54,11 @@ import org.springblade.core.tenant.BladeTenantProperties;
|
|||||||
import org.springblade.system.feign.IUserClient;
|
import org.springblade.system.feign.IUserClient;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -88,6 +91,16 @@ public class BladeAuthConfiguration {
|
|||||||
return new BladeLogHandler(authLogClient, bladeProperties, serverInfo);
|
return new BladeLogHandler(authLogClient, bladeProperties, serverInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<LogoutLogFilter> logoutLogFilter(BladeLogHandler logHandler) {
|
||||||
|
FilterRegistrationBean<LogoutLogFilter> registration = new FilterRegistrationBean<>();
|
||||||
|
registration.setFilter(new LogoutLogFilter(logHandler));
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
registration.setName("logoutLogFilter");
|
||||||
|
registration.setOrder(Ordered.LOWEST_PRECEDENCE - 100);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordHandler passwordHandler(OAuth2Properties properties) {
|
public PasswordHandler passwordHandler(OAuth2Properties properties) {
|
||||||
return new BladePasswordHandler(properties);
|
return new BladePasswordHandler(properties);
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.auth.filter;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springblade.auth.handler.BladeLogHandler;
|
||||||
|
import org.springblade.core.secure.BladeUser;
|
||||||
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录日志过滤器
|
||||||
|
* 在清除 Token 前解析当前用户并异步写入认证日志
|
||||||
|
*
|
||||||
|
* @author BladeX
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LogoutLogFilter extends OncePerRequestFilter implements Ordered {
|
||||||
|
|
||||||
|
private static final String LOGOUT_PATH = "/oauth/logout";
|
||||||
|
|
||||||
|
private final BladeLogHandler logHandler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String path = request.getRequestURI();
|
||||||
|
return StringUtil.isBlank(path) || !path.contains(LOGOUT_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
// 必须在 logout 清 token 之前取用户,否则无法关联账号
|
||||||
|
BladeUser user = AuthUtil.getUser();
|
||||||
|
try {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
if (user != null) {
|
||||||
|
logHandler.handleLogoutLog(user, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return Ordered.LOWEST_PRECEDENCE - 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,12 +29,15 @@ import org.springblade.core.launch.props.BladeProperties;
|
|||||||
import org.springblade.core.launch.server.ServerInfo;
|
import org.springblade.core.launch.server.ServerInfo;
|
||||||
import org.springblade.core.oauth2.provider.OAuth2Request;
|
import org.springblade.core.oauth2.provider.OAuth2Request;
|
||||||
import org.springblade.core.oauth2.service.OAuth2User;
|
import org.springblade.core.oauth2.service.OAuth2User;
|
||||||
|
import org.springblade.core.secure.BladeUser;
|
||||||
import org.springblade.core.tool.utils.DateUtil;
|
import org.springblade.core.tool.utils.DateUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
import org.springblade.core.tool.utils.WebUtil;
|
import org.springblade.core.tool.utils.WebUtil;
|
||||||
import org.springblade.system.pojo.entity.AuthLog;
|
|
||||||
import org.springblade.system.feign.IAuthLogClient;
|
import org.springblade.system.feign.IAuthLogClient;
|
||||||
|
import org.springblade.system.pojo.entity.AuthLog;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -42,7 +45,7 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证日志处理器
|
* 认证日志处理器
|
||||||
* 在用户认证成功时异步记录登录日志
|
* 在用户认证成功/退出时异步记录登录、退出日志
|
||||||
*
|
*
|
||||||
* @author BladeX
|
* @author BladeX
|
||||||
*/
|
*/
|
||||||
@@ -50,6 +53,11 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class BladeLogHandler {
|
public class BladeLogHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录授权类型标识
|
||||||
|
*/
|
||||||
|
public static final String GRANT_TYPE_LOGOUT = "logout";
|
||||||
|
|
||||||
private final IAuthLogClient authLogClient;
|
private final IAuthLogClient authLogClient;
|
||||||
private final BladeProperties bladeProperties;
|
private final BladeProperties bladeProperties;
|
||||||
private final ServerInfo serverInfo;
|
private final ServerInfo serverInfo;
|
||||||
@@ -72,6 +80,26 @@ public class BladeLogHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录退出登录日志
|
||||||
|
*
|
||||||
|
* @param user 当前登录用户
|
||||||
|
* @param request HTTP 请求
|
||||||
|
*/
|
||||||
|
public void handleLogoutLog(BladeUser user, HttpServletRequest request) {
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
AuthLog authLog = buildLogoutLog(user, request);
|
||||||
|
authLogClient.saveAuthLog(authLog);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.error("记录退出日志异常:{}", exception.getMessage(), exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建认证日志实体
|
* 构建认证日志实体
|
||||||
*
|
*
|
||||||
@@ -96,4 +124,38 @@ public class BladeLogHandler {
|
|||||||
return authLog;
|
return authLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建退出日志实体
|
||||||
|
*
|
||||||
|
* @param user 当前登录用户
|
||||||
|
* @param request HTTP 请求
|
||||||
|
* @return AuthLog
|
||||||
|
*/
|
||||||
|
private AuthLog buildLogoutLog(BladeUser user, HttpServletRequest request) {
|
||||||
|
AuthLog authLog = new AuthLog();
|
||||||
|
authLog.setUserId(user.getUserId());
|
||||||
|
authLog.setTenantId(user.getTenantId());
|
||||||
|
authLog.setServiceId(bladeProperties.getName());
|
||||||
|
authLog.setServerIp(serverInfo.getIpWithPort());
|
||||||
|
authLog.setServerHost(serverInfo.getHostName());
|
||||||
|
authLog.setEnv(bladeProperties.getEnv());
|
||||||
|
authLog.setAccount(user.getAccount());
|
||||||
|
authLog.setRealName(resolveRealName(user));
|
||||||
|
authLog.setGrantType(GRANT_TYPE_LOGOUT);
|
||||||
|
authLog.setRemoteIp(WebUtil.getIP(request));
|
||||||
|
authLog.setUserAgent(WebUtil.getUserAgent(request));
|
||||||
|
authLog.setLoginTime(DateUtil.now());
|
||||||
|
return authLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveRealName(BladeUser user) {
|
||||||
|
if (StringUtil.isNotBlank(user.getNickName())) {
|
||||||
|
return user.getNickName();
|
||||||
|
}
|
||||||
|
if (StringUtil.isNotBlank(user.getUserName())) {
|
||||||
|
return user.getUserName();
|
||||||
|
}
|
||||||
|
return user.getAccount();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,25 @@
|
|||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
<artifactId>blade-starter-loadbalancer</artifactId>
|
<artifactId>blade-starter-loadbalancer</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springblade</groupId>
|
||||||
|
<artifactId>blade-starter-log</artifactId>
|
||||||
|
<!-- provided:LenientDateParser 编译期需要 ServiceException(位于 starter-log),
|
||||||
|
但 provided 不向下游传递——否则网关会经 common 拿到 blade-core-tool 的
|
||||||
|
BladeConverterConfiguration(implements WebMvcConfigurer),在无 spring-webmvc
|
||||||
|
的 WebFlux 网关里启动即崩。业务服务经 blade-core-boot 自带 starter-log,运行时不受影响。 -->
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.swagger.core.v3</groupId>
|
||||||
|
<artifactId>swagger-annotations</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
<artifactId>blade-core-auto</artifactId>
|
<artifactId>blade-core-auto</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.common.aspect;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.springblade.core.log.annotation.ApiLog;
|
||||||
|
import org.springblade.core.log.constant.EventConstant;
|
||||||
|
import org.springblade.core.log.event.ApiLogEvent;
|
||||||
|
import org.springblade.core.log.model.LogApi;
|
||||||
|
import org.springblade.core.log.utils.LogAbstractUtil;
|
||||||
|
import org.springblade.core.tool.constant.BladeConstant;
|
||||||
|
import org.springblade.core.tool.utils.SpringUtil;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springblade.core.tool.utils.WebUtil;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增删改操作日志切面
|
||||||
|
* 自动拦截 Controller 中的 save/submit/update/remove/delete 等方法,写入 blade_log_api
|
||||||
|
* 已标注 {@link ApiLog} 的方法跳过,避免重复记录
|
||||||
|
*
|
||||||
|
* @author BladeX
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Aspect
|
||||||
|
public class OperationApiLogAspect {
|
||||||
|
|
||||||
|
@Around("""
|
||||||
|
execution(* org.springblade..controller..*.save*(..))
|
||||||
|
|| execution(* org.springblade..controller..*.submit*(..))
|
||||||
|
|| execution(* org.springblade..controller..*.update*(..))
|
||||||
|
|| execution(* org.springblade..controller..*.remove*(..))
|
||||||
|
|| execution(* org.springblade..controller..*.delete*(..))
|
||||||
|
""")
|
||||||
|
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
|
Method method = signature.getMethod();
|
||||||
|
if (!shouldRecord(method)) {
|
||||||
|
return point.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
String className = point.getTarget().getClass().getName();
|
||||||
|
String methodName = method.getName();
|
||||||
|
String title = resolveTitle(point.getTarget().getClass(), method);
|
||||||
|
long beginTime = System.currentTimeMillis();
|
||||||
|
Object result = point.proceed();
|
||||||
|
long time = System.currentTimeMillis() - beginTime;
|
||||||
|
try {
|
||||||
|
publishEvent(methodName, className, title, time);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("记录操作日志失败: {}#{} - {}", className, methodName, e.getMessage());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldRecord(Method method) {
|
||||||
|
if (method.getAnnotation(ApiLog.class) != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HttpServletRequest request = WebUtil.getRequest();
|
||||||
|
if (request == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String httpMethod = request.getMethod();
|
||||||
|
return !"GET".equalsIgnoreCase(httpMethod)
|
||||||
|
&& !"HEAD".equalsIgnoreCase(httpMethod)
|
||||||
|
&& !"OPTIONS".equalsIgnoreCase(httpMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTitle(Class<?> targetClass, Method method) {
|
||||||
|
Tag tag = AnnotationUtils.findAnnotation(targetClass, Tag.class);
|
||||||
|
Operation operation = AnnotationUtils.findAnnotation(method, Operation.class);
|
||||||
|
String module = tag == null ? null : firstNonBlank(tag.name(), tag.description());
|
||||||
|
String action = operation == null ? null : firstNonBlank(operation.summary(), operation.description());
|
||||||
|
if (StringUtil.isNotBlank(module) && StringUtil.isNotBlank(action)) {
|
||||||
|
return module + "-" + action;
|
||||||
|
}
|
||||||
|
if (StringUtil.isNotBlank(action)) {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
String simpleName = targetClass.getSimpleName().replace("Controller", "");
|
||||||
|
return simpleName + "-" + method.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (StringUtil.isNotBlank(value)) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishEvent(String methodName, String methodClass, String title, long time) {
|
||||||
|
HttpServletRequest request = WebUtil.getRequest();
|
||||||
|
LogApi logApi = new LogApi();
|
||||||
|
logApi.setType(BladeConstant.LOG_NORMAL_TYPE);
|
||||||
|
logApi.setTitle(title);
|
||||||
|
logApi.setTime(String.valueOf(time));
|
||||||
|
logApi.setMethodClass(methodClass);
|
||||||
|
logApi.setMethodName(methodName);
|
||||||
|
LogAbstractUtil.addRequestInfoToLog(request, logApi);
|
||||||
|
Map<String, Object> event = new HashMap<>(16);
|
||||||
|
event.put(EventConstant.EVENT_LOG, logApi);
|
||||||
|
SpringUtil.publishEvent(new ApiLogEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+15
-4
@@ -26,16 +26,27 @@
|
|||||||
package org.springblade.common.config;
|
package org.springblade.common.config;
|
||||||
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import org.springblade.common.aspect.OperationApiLogAspect;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公共封装包配置类
|
* 公共封装包配置类
|
||||||
*
|
*
|
||||||
* @author Chill
|
* @author Chill
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@AutoConfiguration
|
||||||
@AllArgsConstructor
|
|
||||||
public class BladeCommonConfiguration {
|
public class BladeCommonConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||||
|
@ConditionalOnClass(name = "org.springblade.core.log.event.ApiLogEvent")
|
||||||
|
@ConditionalOnProperty(value = "blade.log.operation.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public OperationApiLogAspect operationApiLogAspect() {
|
||||||
|
return new OperationApiLogAspect();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-5
@@ -46,9 +46,11 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入失败明细 Excel 导出工具类
|
* 导入失败明细 Excel 导出工具类
|
||||||
@@ -95,7 +97,7 @@ public class ImportFailureExcelUtil {
|
|||||||
* @param excelClass 原导入 Excel 类型
|
* @param excelClass 原导入 Excel 类型
|
||||||
*/
|
*/
|
||||||
public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) {
|
public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) {
|
||||||
exportFailureReasonOnly(response, fileName, sheetName, data, excelClass);
|
export(response, fileName, sheetName, data, excelClass, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +111,16 @@ public class ImportFailureExcelUtil {
|
|||||||
*/
|
*/
|
||||||
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
|
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
|
||||||
List<?> data, Class<?> excelClass) {
|
List<?> data, Class<?> excelClass) {
|
||||||
|
export(response, fileName, sheetName, data, excelClass, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出导入失败明细
|
||||||
|
*
|
||||||
|
* @param markErrorColumns 是否按失败原因里出现的列名,标红对应字段单元格
|
||||||
|
*/
|
||||||
|
private static void export(HttpServletResponse response, String fileName, String sheetName,
|
||||||
|
List<?> data, Class<?> excelClass, boolean markErrorColumns) {
|
||||||
response.setContentType("application/vnd.ms-excel");
|
response.setContentType("application/vnd.ms-excel");
|
||||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
||||||
@@ -118,7 +130,7 @@ public class ImportFailureExcelUtil {
|
|||||||
List<List<Object>> rows = buildRows(data, excelFields);
|
List<List<Object>> rows = buildRows(data, excelFields);
|
||||||
try {
|
try {
|
||||||
FastExcel.write(response.getOutputStream())
|
FastExcel.write(response.getOutputStream())
|
||||||
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields.size()))
|
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows, markErrorColumns))
|
||||||
.head(head)
|
.head(head)
|
||||||
.sheet(sheetName)
|
.sheet(sheetName)
|
||||||
.doWrite(rows);
|
.doWrite(rows);
|
||||||
@@ -204,15 +216,59 @@ public class ImportFailureExcelUtil {
|
|||||||
throw new NoSuchFieldException(String.join(",", fieldNames));
|
throw new NoSuchFieldException(String.join(",", fieldNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本归一化:去掉空白、星号与中英文标点并统一小写
|
||||||
|
* <p>
|
||||||
|
* 目的是让「失败原因里写的列名」与「表头列名」能直接做包含匹配,
|
||||||
|
* 不受「*」「/」「:」等写法差异影响。
|
||||||
|
*/
|
||||||
|
private static String normalize(String value) {
|
||||||
|
return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》//、-]", "").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取某一列用于定位的候选关键词
|
||||||
|
* <p>
|
||||||
|
* 包含表头列名、字段名,以及列名按分隔符拆出的片段
|
||||||
|
* (如「车牌号/船号」拆出「车牌号」「船号」,失败原因只写其中一段时也能定位)。
|
||||||
|
*/
|
||||||
|
private static List<String> columnKeywords(Field field) {
|
||||||
|
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||||
|
String[] value = excelProperty.value();
|
||||||
|
String columnName = value.length == 0 ? field.getName() : value[0];
|
||||||
|
List<String> 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 static class ImportFailureCellStyleHandler implements CellWriteHandler {
|
||||||
|
|
||||||
private final int failureReasonColumnIndex;
|
private final int failureReasonColumnIndex;
|
||||||
|
private final boolean markErrorColumns;
|
||||||
|
private final List<List<Object>> rows;
|
||||||
|
private final Map<Integer, Set<Integer>> redColumnsByRow = new HashMap<>();
|
||||||
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
|
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
|
||||||
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
|
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
|
||||||
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
|
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
|
||||||
|
|
||||||
private ImportFailureCellStyleHandler(int failureReasonColumnIndex) {
|
private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows, boolean markErrorColumns) {
|
||||||
this.failureReasonColumnIndex = failureReasonColumnIndex;
|
this.failureReasonColumnIndex = excelFields.size();
|
||||||
|
this.markErrorColumns = markErrorColumns;
|
||||||
|
this.rows = rows;
|
||||||
|
if (markErrorColumns) {
|
||||||
|
List<List<String>> columnKeywords = excelFields.stream()
|
||||||
|
.map(ImportFailureExcelUtil::columnKeywords)
|
||||||
|
.toList();
|
||||||
|
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
||||||
|
redColumnsByRow.put(rowIndex, resolveRedColumns(rows.get(rowIndex), columnKeywords));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -230,11 +286,83 @@ public class ImportFailureExcelUtil {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
adjustColumnWidth(cell);
|
adjustColumnWidth(cell);
|
||||||
if (cell.getColumnIndex() == failureReasonColumnIndex) {
|
if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
|
||||||
markRed(cell);
|
markRed(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算某一行需要标红的列
|
||||||
|
* <p>
|
||||||
|
* 失败原因里出现的列名即视为出错列;但当某个命中片段被另一列更长的命中
|
||||||
|
* 片段完全覆盖时(如原因「车船类型不能为空」同时命中「车船类型」和它的
|
||||||
|
* 子串「类型」),只保留更长的那一列,避免把无关列一起标红。
|
||||||
|
*/
|
||||||
|
private Set<Integer> resolveRedColumns(List<Object> row, List<List<String>> columnKeywords) {
|
||||||
|
Set<Integer> redColumns = new LinkedHashSet<>();
|
||||||
|
redColumns.add(failureReasonColumnIndex);
|
||||||
|
String failureReason = normalize(String.valueOf(row.get(failureReasonColumnIndex)));
|
||||||
|
if (failureReason.isEmpty()) {
|
||||||
|
return redColumns;
|
||||||
|
}
|
||||||
|
List<int[]> matches = new ArrayList<>();
|
||||||
|
for (int column = 0; column < columnKeywords.size(); column++) {
|
||||||
|
for (String keyword : columnKeywords.get(column)) {
|
||||||
|
int fromIndex = 0;
|
||||||
|
while (true) {
|
||||||
|
int index = failureReason.indexOf(keyword, fromIndex);
|
||||||
|
if (index < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
matches.add(new int[]{index, index + keyword.length(), column});
|
||||||
|
fromIndex = index + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int[] match : matches) {
|
||||||
|
if (!isSubsumed(match, matches)) {
|
||||||
|
redColumns.add(match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return redColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断该命中片段是否被另一列更长的命中片段完全覆盖
|
||||||
|
*/
|
||||||
|
private boolean isSubsumed(int[] match, List<int[]> matches) {
|
||||||
|
int matchLength = match[1] - match[0];
|
||||||
|
for (int[] other : matches) {
|
||||||
|
if (other[2] == match[2]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((other[1] - other[0]) > matchLength && other[0] <= match[0] && other[1] >= match[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldMarkRed(int rowIndex, int columnIndex) {
|
||||||
|
if (columnIndex == failureReasonColumnIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!markErrorColumns) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Set<Integer> redColumns = redColumnsByRow.get(rowIndex);
|
||||||
|
return redColumns != null && redColumns.contains(columnIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标红单元格
|
||||||
|
* <p>
|
||||||
|
* 统一只改字体颜色,不加底纹;空单元格的红色字体在屏幕上不可见,
|
||||||
|
* 这类错误依靠失败原因列的文字定位。
|
||||||
|
*/
|
||||||
private void markRed(Cell cell) {
|
private void markRed(Cell cell) {
|
||||||
CellStyle currentStyle = cell.getCellStyle();
|
CellStyle currentStyle = cell.getCellStyle();
|
||||||
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
|
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.common.excel;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量导入宽容日期解析器(口径见根工作区 docs/import-spec.md)。
|
||||||
|
* <p>
|
||||||
|
* 解析规则:
|
||||||
|
* <ul>
|
||||||
|
* <li>分隔符 {@code -}、{@code /}、{@code .} 均接受;补零与否均可(2026-08-02 ≡ 2026-8-2)</li>
|
||||||
|
* <li>日期时间:日期部分同上,时间 HH:mm:ss,可省略秒或秒+分(2026-8-2 12:3 可解析)</li>
|
||||||
|
* <li>拒绝:两位年份(26-8-2)、日在前(2/8/2026)、无分隔符(20260802)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
public final class LenientDateParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期部分:4 位年 + 分隔符 + 1~2 位月 + 同一分隔符 + 1~2 位日(年在前,拒绝两位年份与日在前)。
|
||||||
|
*/
|
||||||
|
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})([-/.])(\\d{1,2})\\2(\\d{1,2})");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间部分:1~2 位时[:1~2 位分[:1~2 位秒]],逐级可省略。
|
||||||
|
*/
|
||||||
|
private static final Pattern TIME_PATTERN = Pattern.compile("(\\d{1,2})(?::(\\d{1,2})(?::(\\d{1,2}))?)?");
|
||||||
|
|
||||||
|
private LenientDateParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日期文本,失败返回 {@code null}。
|
||||||
|
*
|
||||||
|
* @param value 单元格原始文本
|
||||||
|
* @return 日期;无法识别时返回 null
|
||||||
|
*/
|
||||||
|
public static LocalDate parseDateOrNull(String value) {
|
||||||
|
String normalized = normalize(value);
|
||||||
|
if (normalized == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Matcher matcher = DATE_PATTERN.matcher(normalized);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.of(Integer.parseInt(matcher.group(1)),
|
||||||
|
Integer.parseInt(matcher.group(3)), Integer.parseInt(matcher.group(4)));
|
||||||
|
} catch (NumberFormatException | java.time.DateTimeException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日期文本;允许携带合法时间部分并截断(如 2026-9-1 8:0:0 按 2026-09-01 解析,
|
||||||
|
* 与 fastexcel 内置 LocalDate 转换的既有宽容度保持一致),失败返回 {@code null}。
|
||||||
|
*
|
||||||
|
* @param value 单元格原始文本
|
||||||
|
* @return 日期;无法识别时返回 null
|
||||||
|
*/
|
||||||
|
public static LocalDate parseDateLenientlyOrNull(String value) {
|
||||||
|
LocalDate date = parseDateOrNull(value);
|
||||||
|
if (date != null) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
LocalDateTime dateTime = parseDateTimeOrNull(value);
|
||||||
|
return dateTime == null ? null : dateTime.toLocalDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日期时间文本;纯日期按当日零点处理,失败返回 {@code null}。
|
||||||
|
*
|
||||||
|
* @param value 单元格原始文本
|
||||||
|
* @return 日期时间;无法识别时返回 null
|
||||||
|
*/
|
||||||
|
public static LocalDateTime parseDateTimeOrNull(String value) {
|
||||||
|
String normalized = normalize(value);
|
||||||
|
if (normalized == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 拆出日期与时间两部分;中间允许 1 个及以上空白或小写 t(2026-8-2t12:3 亦接受)。
|
||||||
|
String[] parts = normalized.split("[ \\t]+|(?<=\\d)t", 2);
|
||||||
|
if (parts.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LocalDate date = parseDateOrNull(parts[0]);
|
||||||
|
if (date == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (parts.length == 1) {
|
||||||
|
return date.atStartOfDay();
|
||||||
|
}
|
||||||
|
Matcher matcher = TIME_PATTERN.matcher(parts[1]);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int hour = Integer.parseInt(matcher.group(1));
|
||||||
|
int minute = matcher.group(2) == null ? 0 : Integer.parseInt(matcher.group(2));
|
||||||
|
int second = matcher.group(3) == null ? 0 : Integer.parseInt(matcher.group(3));
|
||||||
|
return LocalDateTime.of(date, LocalTime.of(hour, minute, second));
|
||||||
|
} catch (NumberFormatException | java.time.DateTimeException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日期文本,失败抛出携带口径文案的 {@link org.springblade.core.log.exception.ServiceException}。
|
||||||
|
* 空白与 {@code null} 返回 {@code null}(可选字段由业务校验决定是否必填)。
|
||||||
|
*
|
||||||
|
* @param value 单元格原始文本
|
||||||
|
* @param columnName 导入模板列名(用于失败原因文案与失败明细标红定位)
|
||||||
|
* @return 日期
|
||||||
|
*/
|
||||||
|
public static LocalDate parseDate(String value, String columnName) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LocalDate date = parseDateLenientlyOrNull(value);
|
||||||
|
if (date == null) {
|
||||||
|
throw unrecognizedDate(value, columnName);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日期时间文本,失败抛出携带口径文案的 {@link org.springblade.core.log.exception.ServiceException}。
|
||||||
|
* 空白与 {@code null} 返回 {@code null}(可选字段由业务校验决定是否必填)。
|
||||||
|
*
|
||||||
|
* @param value 单元格原始文本
|
||||||
|
* @param columnName 导入模板列名(用于失败原因文案与失败明细标红定位)
|
||||||
|
* @return 日期时间
|
||||||
|
*/
|
||||||
|
public static LocalDateTime parseDateTime(String value, String columnName) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LocalDateTime dateTime = parseDateTimeOrNull(value);
|
||||||
|
if (dateTime == null) {
|
||||||
|
throw unrecognizedDate(value, columnName);
|
||||||
|
}
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按口径构造「日期格式无法识别」错误。
|
||||||
|
*
|
||||||
|
* @param value 单元格原始文本
|
||||||
|
* @param columnName 导入模板列名
|
||||||
|
* @return 业务异常
|
||||||
|
*/
|
||||||
|
public static org.springblade.core.log.exception.ServiceException unrecognizedDate(String value, String columnName) {
|
||||||
|
String displayName = columnName == null || columnName.isBlank() ? "日期" : columnName;
|
||||||
|
return new org.springblade.core.log.exception.ServiceException(
|
||||||
|
displayName + " 日期格式无法识别:" + (value == null ? "" : value.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化输入:去首尾空白,跳过空白值。
|
||||||
|
*/
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = value.trim();
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.common.excel;
|
||||||
|
|
||||||
|
import cn.idev.excel.converters.Converter;
|
||||||
|
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||||
|
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||||
|
import cn.idev.excel.metadata.data.ReadCellData;
|
||||||
|
import cn.idev.excel.metadata.data.WriteCellData;
|
||||||
|
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||||
|
import cn.idev.excel.util.DateUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量导入宽容日期转换器(String 承载):Excel 数值日期(序列号)转 ISO 文本,文本原样透传。
|
||||||
|
* <p>
|
||||||
|
* 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
|
||||||
|
* 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
|
||||||
|
* 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
public class LenientDateStringConverter implements Converter<String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> supportJavaTypeKey() {
|
||||||
|
return String.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CellDataTypeEnum supportExcelTypeKey() {
|
||||||
|
return CellDataTypeEnum.STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
|
||||||
|
GlobalConfiguration globalConfiguration) {
|
||||||
|
if (cellData.getType() == CellDataTypeEnum.NUMBER) {
|
||||||
|
LocalDate date = DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(),
|
||||||
|
globalConfiguration.getUse1904windowing()).toLocalDate();
|
||||||
|
return date.toString();
|
||||||
|
}
|
||||||
|
return cellData.getStringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
|
||||||
|
GlobalConfiguration globalConfiguration) {
|
||||||
|
return new WriteCellData<>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.common.excel;
|
||||||
|
|
||||||
|
import cn.idev.excel.converters.Converter;
|
||||||
|
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||||
|
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||||
|
import cn.idev.excel.metadata.data.ReadCellData;
|
||||||
|
import cn.idev.excel.metadata.data.WriteCellData;
|
||||||
|
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||||
|
import cn.idev.excel.util.DateUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量导入宽容日期时间转换器(String 承载):Excel 数值日期(序列号)转
|
||||||
|
* {@code yyyy-MM-dd HH:mm:ss} 文本,文本原样透传。
|
||||||
|
* <p>
|
||||||
|
* 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
|
||||||
|
* 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
|
||||||
|
* 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
public class LenientDateTimeStringConverter implements Converter<String> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数值日期序列号转文本的输出格式,与导入模板展示格式保持一致。
|
||||||
|
*/
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> supportJavaTypeKey() {
|
||||||
|
return String.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CellDataTypeEnum supportExcelTypeKey() {
|
||||||
|
return CellDataTypeEnum.STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
|
||||||
|
GlobalConfiguration globalConfiguration) {
|
||||||
|
if (cellData.getType() == CellDataTypeEnum.NUMBER) {
|
||||||
|
LocalDateTime dateTime = DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(),
|
||||||
|
globalConfiguration.getUse1904windowing());
|
||||||
|
return dateTime.format(DATE_TIME_FORMATTER);
|
||||||
|
}
|
||||||
|
return cellData.getStringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
|
||||||
|
GlobalConfiguration globalConfiguration) {
|
||||||
|
return new WriteCellData<>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.common.utils;
|
||||||
|
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录密码强度规则工具
|
||||||
|
* 规则:大于8位,同时包含字母、数字、特殊字符(.!@#$%^&*)
|
||||||
|
*
|
||||||
|
* @author BladeX
|
||||||
|
*/
|
||||||
|
public final class PasswordRuleUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许的特殊字符集合
|
||||||
|
*/
|
||||||
|
public static final String SPECIAL_CHARS = ".!@#$%^&*";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小长度(大于 8 位 => 至少 9 位)
|
||||||
|
*/
|
||||||
|
public static final int MIN_LENGTH = 9;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动生成密码长度
|
||||||
|
*/
|
||||||
|
public static final int GENERATE_LENGTH = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则提示文案
|
||||||
|
*/
|
||||||
|
public static final String RULE_MESSAGE = "密码须大于8位,且同时包含字母、数字和特殊字符(.!@#$%^&*)";
|
||||||
|
|
||||||
|
private static final String LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
private static final String DIGITS = "23456789";
|
||||||
|
private static final String ALL_CHARS = LETTERS + DIGITS + SPECIAL_CHARS;
|
||||||
|
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
|
||||||
|
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[.!@#$%^&*]).{" + MIN_LENGTH + ",}$"
|
||||||
|
);
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
private PasswordRuleUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验密码是否符合强度规则
|
||||||
|
*
|
||||||
|
* @param password 明文密码
|
||||||
|
* @return true-符合
|
||||||
|
*/
|
||||||
|
public static boolean isValid(String password) {
|
||||||
|
return StringUtil.isNotBlank(password) && PASSWORD_PATTERN.matcher(password).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成符合规则的随机密码
|
||||||
|
*
|
||||||
|
* @return 随机密码
|
||||||
|
*/
|
||||||
|
public static String generate() {
|
||||||
|
List<Character> chars = new ArrayList<>(GENERATE_LENGTH);
|
||||||
|
chars.add(LETTERS.charAt(SECURE_RANDOM.nextInt(LETTERS.length())));
|
||||||
|
chars.add(DIGITS.charAt(SECURE_RANDOM.nextInt(DIGITS.length())));
|
||||||
|
chars.add(SPECIAL_CHARS.charAt(SECURE_RANDOM.nextInt(SPECIAL_CHARS.length())));
|
||||||
|
for (int i = chars.size(); i < GENERATE_LENGTH; i++) {
|
||||||
|
chars.add(ALL_CHARS.charAt(SECURE_RANDOM.nextInt(ALL_CHARS.length())));
|
||||||
|
}
|
||||||
|
Collections.shuffle(chars, SECURE_RANDOM);
|
||||||
|
StringBuilder password = new StringBuilder(GENERATE_LENGTH);
|
||||||
|
for (Character ch : chars) {
|
||||||
|
password.append(ch);
|
||||||
|
}
|
||||||
|
return password.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.springblade.common.config.BladeCommonConfiguration
|
||||||
+12
@@ -39,6 +39,7 @@ public interface IBusinessProcessClient {
|
|||||||
String QUERY_BUSINESS_PROCESS_SNAPSHOT = API_PREFIX + "/queryBusinessProcessSnapshot";
|
String QUERY_BUSINESS_PROCESS_SNAPSHOT = API_PREFIX + "/queryBusinessProcessSnapshot";
|
||||||
String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments";
|
String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments";
|
||||||
String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes";
|
String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes";
|
||||||
|
String GET_PROCESS_INFO = API_PREFIX + "/getProcessInfo";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交业务流程
|
* 提交业务流程
|
||||||
@@ -114,4 +115,15 @@ public interface IBusinessProcessClient {
|
|||||||
@GetMapping(GET_CURRENT_NODES)
|
@GetMapping(GET_CURRENT_NODES)
|
||||||
FR<Object> getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId,
|
FR<Object> getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId,
|
||||||
@RequestParam(value = "loginName", required = false) String loginName);
|
@RequestParam(value = "loginName", required = false) String loginName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取流程实例详情
|
||||||
|
*
|
||||||
|
* @param processInstanceId 流程实例id
|
||||||
|
* @param loginName MK登录名(手机号)
|
||||||
|
* @return 流程实例详情
|
||||||
|
*/
|
||||||
|
@GetMapping(GET_PROCESS_INFO)
|
||||||
|
FR<Object> getProcessInfo(@RequestParam("processInstanceId") String processInstanceId,
|
||||||
|
@RequestParam(value = "loginName", required = false) String loginName);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -25,10 +25,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.springblade.system.pojo.entity;
|
package org.springblade.system.pojo.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -185,4 +188,20 @@ public class Dept extends TenantEntity {
|
|||||||
@Schema(description = "是否平台公司:0否,1是")
|
@Schema(description = "是否平台公司:0否,1是")
|
||||||
private Integer isPlatformCompany;
|
private Integer isPlatformCompany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OA主键,仅同步记录,不对外展示
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
@Schema(description = "OA主键", hidden = true)
|
||||||
|
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
|
||||||
|
private String oaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OA上级主键,仅同步记录,不对外展示
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
@Schema(description = "OA上级主键", hidden = true)
|
||||||
|
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
|
||||||
|
private String oaSupSubComId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package org.springblade.transport.feign;
|
||||||
|
|
||||||
|
import org.springblade.core.tool.api.FR;
|
||||||
|
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客商档案 Feign接口
|
||||||
|
*/
|
||||||
|
@FeignClient(value = "blade-transport")
|
||||||
|
public interface ICustomerArchiveClient {
|
||||||
|
|
||||||
|
String API_PREFIX = "/feign/client/customerArchive";
|
||||||
|
String SYNC_PROCESS_NODE = API_PREFIX + "/syncProcessNode";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 MK 当前节点同步客商当前节点、当前处理人和审批状态
|
||||||
|
*
|
||||||
|
* @param param 同步参数
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
@PostMapping(SYNC_PROCESS_NODE)
|
||||||
|
FR<Boolean> syncProcessNode(@RequestBody CustomerProcessNodeSyncDTO param);
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package org.springblade.transport.feign;
|
||||||
|
|
||||||
|
import org.springblade.core.tool.api.FR;
|
||||||
|
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MK 业务流程同步 Feign
|
||||||
|
*/
|
||||||
|
@FeignClient(value = "blade-transport")
|
||||||
|
public interface IMkProcessClient {
|
||||||
|
|
||||||
|
String API_PREFIX = "/feign/client/mkProcess";
|
||||||
|
String APPLY = API_PREFIX + "/apply";
|
||||||
|
|
||||||
|
@PostMapping(APPLY)
|
||||||
|
FR<Boolean> apply(@RequestBody MkProcessSyncDTO param);
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
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 CustomerProcessNodeSyncDTO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "客商ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "当前节点")
|
||||||
|
private String currentNode;
|
||||||
|
|
||||||
|
@Schema(description = "当前处理人")
|
||||||
|
private String currentProcessor;
|
||||||
|
|
||||||
|
@Schema(description = "审批状态")
|
||||||
|
private String approvalStatus;
|
||||||
|
|
||||||
|
@Schema(description = "MK 流程实例详情,传入后按 currentHandlers.fdName 和 handlerInfos.nodeName 回写")
|
||||||
|
private Object processInfo;
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MK 流程节点同步参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "MK 流程节点同步参数")
|
||||||
|
public class MkProcessSyncDTO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "业务类型,如 project-apply")
|
||||||
|
private String bizType;
|
||||||
|
|
||||||
|
@Schema(description = "业务主键")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "动作:sync / approve / reject")
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
@Schema(description = "处理人姓名")
|
||||||
|
private String processorName;
|
||||||
|
|
||||||
|
@Schema(description = "MK 流程实例详情")
|
||||||
|
private Object processInfo;
|
||||||
|
}
|
||||||
+3
@@ -59,6 +59,9 @@ public class CustomerReceiptAccount extends TenantEntity {
|
|||||||
@Schema(description = "开户行名称")
|
@Schema(description = "开户行名称")
|
||||||
private String bankName;
|
private String bankName;
|
||||||
|
|
||||||
|
@Schema(description = "联行号")
|
||||||
|
private String cnapsCode;
|
||||||
|
|
||||||
@Schema(description = "银行账号")
|
@Schema(description = "银行账号")
|
||||||
private String bankAccount;
|
private String bankAccount;
|
||||||
|
|
||||||
|
|||||||
+14
@@ -30,6 +30,8 @@ import org.springblade.transport.pojo.entity.ProjectApply;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目立项视图实体类
|
* 项目立项视图实体类
|
||||||
@@ -104,4 +106,16 @@ public class ProjectApplyVO extends ProjectApply {
|
|||||||
@Schema(description = "风险计算额度基数")
|
@Schema(description = "风险计算额度基数")
|
||||||
private BigDecimal maxFundLimit;
|
private BigDecimal maxFundLimit;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
@Schema(description = "货物类型字典,公开页回显使用")
|
||||||
|
private List<Map<String, String>> cargoTypeOptions;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
@Schema(description = "运输类型字典,公开页回显使用")
|
||||||
|
private List<Map<String, String>> transportTypeOptions;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
@Schema(description = "结算方式字典,公开页回显使用")
|
||||||
|
private List<Map<String, String>> settlementModeOptions;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,10 @@
|
|||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
<artifactId>blade-mk-api</artifactId>
|
<artifactId>blade-mk-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springblade</groupId>
|
||||||
|
<artifactId>blade-transport-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 其他依赖 -->
|
<!-- 其他依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
+6
@@ -60,6 +60,12 @@ public class BusinessProcessController extends BladeController {
|
|||||||
return R.data(businessProcessService.processSubmit(param));
|
return R.data(businessProcessService.processSubmit(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/processDelete")
|
||||||
|
@ApiOperationSupport(order = 7)
|
||||||
|
public R<Boolean> processDelete(@RequestBody MKProcessCreateDTO param) {
|
||||||
|
return R.data(businessProcessService.processDelete(param == null ? null : param.getFormInstanceId()));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/isEditView")
|
@GetMapping("/isEditView")
|
||||||
@ApiOperationSupport(order = 3)
|
@ApiOperationSupport(order = 3)
|
||||||
@Operation(summary = "是否编辑页", description = "传入业务id")
|
@Operation(summary = "是否编辑页", description = "传入业务id")
|
||||||
|
|||||||
+8
@@ -86,4 +86,12 @@ public class BusinessProcessClient implements IBusinessProcessClient {
|
|||||||
@RequestParam(value = "loginName", required = false) String loginName) {
|
@RequestParam(value = "loginName", required = false) String loginName) {
|
||||||
return FR.data(businessProcessService.getCurrentNodes(processInstanceId, loginName));
|
return FR.data(businessProcessService.getCurrentNodes(processInstanceId, loginName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||||
|
@GetMapping(GET_PROCESS_INFO)
|
||||||
|
@Override
|
||||||
|
public FR<Object> getProcessInfo(@RequestParam("processInstanceId") String processInstanceId,
|
||||||
|
@RequestParam(value = "loginName", required = false) String loginName) {
|
||||||
|
return FR.data(businessProcessService.getProcessInfo(processInstanceId, loginName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -34,6 +34,14 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
|
|||||||
*/
|
*/
|
||||||
String processSubmit(MKProcessCreateDTO param);
|
String processSubmit(MKProcessCreateDTO param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按业务表单实例 id 删除 MK 流程(不删除本地业务流程记录,供驳回后重新提交使用)
|
||||||
|
*
|
||||||
|
* @param formInstanceId 业务表单实例 id
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean processDelete(String formInstanceId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取流程当前节点详情
|
* 获取流程当前节点详情
|
||||||
*
|
*
|
||||||
@@ -43,6 +51,15 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
|
|||||||
*/
|
*/
|
||||||
List<?> getCurrentNodes(String processInstanceId, String loginName);
|
List<?> getCurrentNodes(String processInstanceId, String loginName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取流程实例详情
|
||||||
|
*
|
||||||
|
* @param processInstanceId 流程实例id
|
||||||
|
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
|
||||||
|
* @return 流程实例详情
|
||||||
|
*/
|
||||||
|
Object getProcessInfo(String processInstanceId, String loginName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改业务流程状态
|
* 修改业务流程状态
|
||||||
* @param param
|
* @param param
|
||||||
|
|||||||
+166
-2
@@ -15,6 +15,7 @@ import org.springblade.process.pojo.enums.ApproveStatusEnum;
|
|||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.log.utils.AssertUtils;
|
import org.springblade.core.log.utils.AssertUtils;
|
||||||
import org.springblade.core.secure.utils.AuthUtil;
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
|
import org.springblade.core.tool.api.FR;
|
||||||
import org.springblade.core.tool.utils.StringUtil;
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
import org.springblade.process.convert.ApprovalConvert;
|
import org.springblade.process.convert.ApprovalConvert;
|
||||||
import org.springblade.process.convert.BusinessProcessConvert;
|
import org.springblade.process.convert.BusinessProcessConvert;
|
||||||
@@ -41,12 +42,17 @@ import org.springblade.thirdparty.mk.pojo.dto.approval.MKProcessDTO;
|
|||||||
import org.springblade.thirdparty.mk.pojo.dto.sort.*;
|
import org.springblade.thirdparty.mk.pojo.dto.sort.*;
|
||||||
import org.springblade.thirdparty.mk.pojo.vo.*;
|
import org.springblade.thirdparty.mk.pojo.vo.*;
|
||||||
import org.springblade.thirdparty.mk.service.IMKService;
|
import org.springblade.thirdparty.mk.service.IMKService;
|
||||||
|
import org.springblade.transport.feign.ICustomerArchiveClient;
|
||||||
|
import org.springblade.transport.feign.IMkProcessClient;
|
||||||
|
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
|
||||||
|
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业务流程关联表 服务实现类
|
* 业务流程关联表 服务实现类
|
||||||
@@ -64,6 +70,8 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
|||||||
private final MKProperties mkProperties;
|
private final MKProperties mkProperties;
|
||||||
private final ApprovalConvert approvalConvert;
|
private final ApprovalConvert approvalConvert;
|
||||||
private final IUserService userService;
|
private final IUserService userService;
|
||||||
|
private final ICustomerArchiveClient customerArchiveClient;
|
||||||
|
private final IMkProcessClient mkProcessClient;
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
@@ -128,8 +136,17 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
|||||||
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
|
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
|
||||||
param.setSubmitIdentity(loginName);
|
param.setSubmitIdentity(loginName);
|
||||||
param.setLoginName(loginName);
|
param.setLoginName(loginName);
|
||||||
log.info("调用mk processSubmit 参数:{}", JSON.toJSONString(param));
|
String bizType = param.getBizType();
|
||||||
String processInstanceId = mkService.processSubmit(param);
|
MKProcessCreateDTO mkParam = new MKProcessCreateDTO();
|
||||||
|
mkParam.setFormInstanceId(param.getFormInstanceId());
|
||||||
|
mkParam.setSubject(param.getSubject());
|
||||||
|
mkParam.setSubmitIdentity(loginName);
|
||||||
|
mkParam.setLoginName(loginName);
|
||||||
|
mkParam.setTemplateCode(param.getTemplateCode());
|
||||||
|
mkParam.setFormValues(param.getFormValues());
|
||||||
|
mkParam.setTempVarData(param.getTempVarData());
|
||||||
|
log.info("调用mk processSubmit 参数:{}", JSON.toJSONString(mkParam));
|
||||||
|
String processInstanceId = mkService.processSubmit(mkParam);
|
||||||
AssertUtils.notBlank(processInstanceId, "提交流程失败,未返回流程实例id");
|
AssertUtils.notBlank(processInstanceId, "提交流程失败,未返回流程实例id");
|
||||||
|
|
||||||
Long bizId;
|
Long bizId;
|
||||||
@@ -164,9 +181,42 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
|||||||
businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue());
|
businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue());
|
||||||
this.saveOrUpdate(businessProcess);
|
this.saveOrUpdate(businessProcess);
|
||||||
this.getCurrentNodes(processInstanceId, loginName);
|
this.getCurrentNodes(processInstanceId, loginName);
|
||||||
|
Object processInfo = this.getProcessInfo(processInstanceId, loginName);
|
||||||
|
this.syncBizFromProcessInfo(bizType, param.getFormInstanceId(), processInfo);
|
||||||
return processInstanceId;
|
return processInstanceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public boolean processDelete(String formInstanceId) {
|
||||||
|
AssertUtils.notBlank(formInstanceId, "表单实例id不能为空");
|
||||||
|
Long bizId;
|
||||||
|
try {
|
||||||
|
bizId = Long.valueOf(formInstanceId);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ServiceException("表单实例id格式不正确");
|
||||||
|
}
|
||||||
|
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
|
||||||
|
.eq(BusinessProcess::getBizId, bizId)
|
||||||
|
);
|
||||||
|
if (businessProcess == null || StringUtil.isBlank(businessProcess.getProcessInstanceId())) {
|
||||||
|
log.warn("客商驳回后删除流程跳过,未找到流程实例 formInstanceId={}", formInstanceId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String loginName = businessProcess.getPromoterLoginName();
|
||||||
|
if (StringUtil.isBlank(loginName)) {
|
||||||
|
loginName = resolveCurrentUserPhone();
|
||||||
|
}
|
||||||
|
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流");
|
||||||
|
log.info("调用mk processDelete processInstanceId={} loginName={} formInstanceId={}",
|
||||||
|
businessProcess.getProcessInstanceId(), loginName, formInstanceId);
|
||||||
|
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
|
||||||
|
if (!deleted) {
|
||||||
|
throw new ServiceException("删除MK流程失败");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<?> getCurrentNodes(String processInstanceId, String loginName) {
|
public List<?> getCurrentNodes(String processInstanceId, String loginName) {
|
||||||
if (StringUtils.isBlank(processInstanceId)) {
|
if (StringUtils.isBlank(processInstanceId)) {
|
||||||
@@ -193,6 +243,120 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getProcessInfo(String processInstanceId, String loginName) {
|
||||||
|
if (StringUtils.isBlank(processInstanceId)) {
|
||||||
|
log.warn("查询流程实例详情失败,processInstanceId为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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 null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Object processInfo = mkService.getProcessInfo(processInstanceId, resolvedLoginName);
|
||||||
|
log.info("获取流程实例详情 processInstanceId={} loginName={} result={}",
|
||||||
|
processInstanceId, resolvedLoginName, JSON.toJSONString(processInfo));
|
||||||
|
return processInfo;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询流程实例详情异常 processInstanceId={} loginName={}", processInstanceId, resolvedLoginName, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncBizFromProcessInfo(String bizType, String formInstanceId, Object processInfo) {
|
||||||
|
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
|
||||||
|
log.warn("同步业务当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Long bizId;
|
||||||
|
try {
|
||||||
|
bizId = Long.valueOf(formInstanceId);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("同步业务当前节点失败,表单实例id不是数字:{}", formInstanceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String resolvedBizType = StringUtils.isBlank(bizType) ? "customer-archive" : bizType;
|
||||||
|
if ("customer-archive".equals(resolvedBizType)) {
|
||||||
|
this.syncCustomerArchiveFromProcessInfo(formInstanceId, processInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MkProcessSyncDTO param = new MkProcessSyncDTO();
|
||||||
|
param.setBizType(resolvedBizType);
|
||||||
|
param.setId(bizId);
|
||||||
|
param.setAction("sync");
|
||||||
|
param.setProcessInfo(processInfo);
|
||||||
|
try {
|
||||||
|
FR<Boolean> result = mkProcessClient.apply(param);
|
||||||
|
log.info("同步业务当前节点完成 bizType={} bizId={} result={}",
|
||||||
|
resolvedBizType, bizId, JSON.toJSONString(result));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("同步业务当前节点异常 bizType={} bizId={}", resolvedBizType, bizId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncCustomerArchiveFromProcessInfo(String formInstanceId, Object processInfo) {
|
||||||
|
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
|
||||||
|
log.warn("同步客商当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Long customerId;
|
||||||
|
try {
|
||||||
|
customerId = Long.valueOf(formInstanceId);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("同步客商当前节点失败,表单实例id不是数字:{}", formInstanceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CustomerProcessNodeSyncDTO param = new CustomerProcessNodeSyncDTO();
|
||||||
|
param.setId(customerId);
|
||||||
|
param.setProcessInfo(processInfo);
|
||||||
|
param.setApprovalStatus("reviewing");
|
||||||
|
try {
|
||||||
|
FR<Boolean> result = customerArchiveClient.syncProcessNode(param);
|
||||||
|
log.info("同步客商当前节点完成 customerId={} result={}", customerId, JSON.toJSONString(result));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("同步客商当前节点异常 customerId={}", customerId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readMkNodeName(Object node) {
|
||||||
|
if (node instanceof MKNodeVO vo) {
|
||||||
|
return vo.getNodeName();
|
||||||
|
}
|
||||||
|
if (node instanceof Map<?, ?> map) {
|
||||||
|
Object value = map.get("nodeName");
|
||||||
|
return value == null ? null : String.valueOf(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<String> readMkHandlerNames(Object node) {
|
||||||
|
List<MKNodeHandlerVO> handlers = null;
|
||||||
|
if (node instanceof MKNodeVO vo) {
|
||||||
|
handlers = vo.getNodeHandlers();
|
||||||
|
} else if (node instanceof Map<?, ?> map && map.get("nodeHandlers") instanceof List<?> list) {
|
||||||
|
return list.stream().map(item -> {
|
||||||
|
if (item instanceof MKNodeHandlerVO handler) {
|
||||||
|
return handler.getHandlerName();
|
||||||
|
}
|
||||||
|
if (item instanceof Map<?, ?> handlerMap) {
|
||||||
|
Object value = handlerMap.get("handlerName");
|
||||||
|
return value == null ? null : String.valueOf(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isEmpty(handlers)) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
return handlers.stream().map(MKNodeHandlerVO::getHandlerName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从当前登录用户实体读取真实手机号(绕过接口返回脱敏)
|
* 从当前登录用户实体读取真实手机号(绕过接口返回脱敏)
|
||||||
*/
|
*/
|
||||||
|
|||||||
+14
@@ -173,6 +173,20 @@ public class DeptController extends BladeController {
|
|||||||
return R.data(deptService.syncIamOrganizations());
|
return R.data(deptService.syncIamOrganizations());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除非顶级组织,供自动同步前确认后调用
|
||||||
|
*/
|
||||||
|
@IsAdmin
|
||||||
|
@PostMapping("/clear-non-top")
|
||||||
|
@ApiOperationSupport(order = 8)
|
||||||
|
@Operation(summary = "清除非顶级组织")
|
||||||
|
public R<Integer> clearNonTopDepts() {
|
||||||
|
int clearedCount = deptService.clearNonTopDepts();
|
||||||
|
CacheUtil.clear(SYS_CACHE);
|
||||||
|
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||||
|
return R.data(clearedCount);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从OA按页同步公司
|
* 从OA按页同步公司
|
||||||
*/
|
*/
|
||||||
|
|||||||
+16
-3
@@ -43,6 +43,8 @@ import org.springblade.core.tenant.annotation.NonDS;
|
|||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
import org.springblade.core.tool.utils.DateUtil;
|
import org.springblade.core.tool.utils.DateUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
|
import org.springblade.common.excel.ImportFailureExcelUtil;
|
||||||
|
import org.springblade.system.excel.ImportFailureException;
|
||||||
import org.springblade.system.excel.PortTerminalExcel;
|
import org.springblade.system.excel.PortTerminalExcel;
|
||||||
import org.springblade.system.excel.PortTerminalExportExcel;
|
import org.springblade.system.excel.PortTerminalExportExcel;
|
||||||
import org.springblade.system.excel.PortTerminalImporter;
|
import org.springblade.system.excel.PortTerminalImporter;
|
||||||
@@ -175,14 +177,25 @@ public class PortTerminalController extends BladeController {
|
|||||||
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||||
return R.fail("请上传 .xls,.xlsx 标准格式文件");
|
return R.fail("请上传 .xls,.xlsx 标准格式文件");
|
||||||
}
|
}
|
||||||
List<PortTerminalExcel> failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
|
try {
|
||||||
if (Func.isNotEmpty(failureList)) {
|
portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
|
||||||
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
|
} catch (ImportFailureException exception) {
|
||||||
|
// 全失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
|
||||||
|
exportFailure(response, exception.getFailureList());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return R.success("操作成功");
|
return R.success("操作成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
|
||||||
|
* <p>
|
||||||
|
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
|
||||||
|
*/
|
||||||
|
private void exportFailure(HttpServletResponse response, List<?> failureList) {
|
||||||
|
ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出港口码头主数据
|
* 导出港口码头主数据
|
||||||
*/
|
*/
|
||||||
|
|||||||
+10
@@ -80,6 +80,9 @@ public interface DeptConvert {
|
|||||||
}
|
}
|
||||||
// 公司所属公司编码就是他自己的部门编码
|
// 公司所属公司编码就是他自己的部门编码
|
||||||
dept.setBelongCompanyCode(dept.getDeptCode());
|
dept.setBelongCompanyCode(dept.getDeptCode());
|
||||||
|
// OA主键与上级OA主键,分批同步结束后用于重建层级
|
||||||
|
dept.setOaId(company.getId());
|
||||||
|
dept.setOaSupSubComId(company.getSupsubcomid());
|
||||||
// 显示顺序处理
|
// 显示顺序处理
|
||||||
dept.setSort(OAUtils.parseInt(company.getShoworder()));
|
dept.setSort(OAUtils.parseInt(company.getShoworder()));
|
||||||
// 部门类型,公司
|
// 部门类型,公司
|
||||||
@@ -100,6 +103,13 @@ public interface DeptConvert {
|
|||||||
Dept dept = this.baseConvert(oaDept);
|
Dept dept = this.baseConvert(oaDept);
|
||||||
// 部门编码添加前缀
|
// 部门编码添加前缀
|
||||||
dept.setDeptCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + oaDept.getId());
|
dept.setDeptCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + oaDept.getId());
|
||||||
|
// OA主键;上级为根部门时记录所属公司OA主键,否则记录上级部门OA主键
|
||||||
|
dept.setOaId(oaDept.getId());
|
||||||
|
if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
|
||||||
|
dept.setOaSupSubComId(oaDept.getSubcompanyid1());
|
||||||
|
} else {
|
||||||
|
dept.setOaSupSubComId(oaDept.getSupdepid());
|
||||||
|
}
|
||||||
// 部门所属公司编码就是他所属公司编码加前缀
|
// 部门所属公司编码就是他所属公司编码加前缀
|
||||||
dept.setBelongCompanyCode(OAConvertConstant.COMPANY_OA_PREFIX + dept.getBelongCompanyCode());
|
dept.setBelongCompanyCode(OAConvertConstant.COMPANY_OA_PREFIX + dept.getBelongCompanyCode());
|
||||||
String parentCode = dept.getParentCode();
|
String parentCode = dept.getParentCode();
|
||||||
|
|||||||
+4
-5
@@ -35,7 +35,6 @@ import lombok.Data;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 币种汇率 Excel
|
* 币种汇率 Excel
|
||||||
@@ -62,8 +61,8 @@ public class CurrencyExcel implements Serializable {
|
|||||||
@ExcelProperty("汇率")
|
@ExcelProperty("汇率")
|
||||||
private BigDecimal exchangeRate;
|
private BigDecimal exchangeRate;
|
||||||
|
|
||||||
@ExcelProperty("生效日期")
|
@ExcelProperty(value = "生效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate effectiveDate;
|
private String effectiveDate;
|
||||||
|
|
||||||
@ExcelProperty("状态")
|
@ExcelProperty("状态")
|
||||||
private String statusName;
|
private String statusName;
|
||||||
@@ -71,8 +70,8 @@ public class CurrencyExcel implements Serializable {
|
|||||||
@ExcelProperty("来源")
|
@ExcelProperty("来源")
|
||||||
private String dataSource;
|
private String dataSource;
|
||||||
|
|
||||||
@ExcelProperty("失效日期")
|
@ExcelProperty(value = "失效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate expiryDate;
|
private String expiryDate;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.system.excel;
|
||||||
|
|
||||||
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入失败异常,携带失败明细用于导出原表并标注错误。
|
||||||
|
* <p>
|
||||||
|
* 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
|
||||||
|
* 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
public class ImportFailureException extends ServiceException {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败明细,包含原表全部数据,错误行已标注错误原因。
|
||||||
|
*/
|
||||||
|
private final transient List<?> failureList;
|
||||||
|
|
||||||
|
public ImportFailureException(List<?> failureList) {
|
||||||
|
super("导入失败,已回滚全部数据");
|
||||||
|
this.failureList = failureList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<?> getFailureList() {
|
||||||
|
return failureList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+2
-3
@@ -34,7 +34,6 @@ import lombok.Data;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserExcel
|
* UserExcel
|
||||||
@@ -102,7 +101,7 @@ public class UserExcel implements Serializable {
|
|||||||
private String postName;
|
private String postName;
|
||||||
|
|
||||||
@ColumnWidth(20)
|
@ColumnWidth(20)
|
||||||
@ExcelProperty("生日")
|
@ExcelProperty(value = "生日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private Date birthday;
|
private String birthday;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="exportUser" resultType="org.springblade.system.excel.UserExcel">
|
<select id="exportUser" resultType="org.springblade.system.excel.UserExcel">
|
||||||
SELECT id, tenant_id, user_type, account, name, real_name, email, phone, birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment}
|
SELECT id, tenant_id, user_type, account, name, real_name, email, phone, DATE_FORMAT(birthday, '%Y-%m-%d') AS birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectCustomerOptions" resultType="java.util.HashMap">
|
<select id="selectCustomerOptions" resultType="java.util.HashMap">
|
||||||
|
|||||||
+19
@@ -146,6 +146,13 @@ public interface IDeptService extends IService<Dept> {
|
|||||||
*/
|
*/
|
||||||
boolean removeDept(String ids);
|
boolean removeDept(String ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除非顶级组织,顶级组织(父级为 0)保留
|
||||||
|
*
|
||||||
|
* @return 删除数量
|
||||||
|
*/
|
||||||
|
int clearNonTopDepts();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增或修改部门
|
* 新增或修改部门
|
||||||
* <p>
|
* <p>
|
||||||
@@ -187,4 +194,16 @@ public interface IDeptService extends IService<Dept> {
|
|||||||
*/
|
*/
|
||||||
void updateAncestors(Date startTime);
|
void updateAncestors(Date startTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 OA 主键与上级 OA 主键重建组织、部门父子关系,并刷新祖级列表
|
||||||
|
*/
|
||||||
|
void rebuildOaHierarchy();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 OA 同步后无法挂到上级的组织(上级 OA 主键在本地不存在),并级联删除其子级
|
||||||
|
*
|
||||||
|
* @return 删除数量
|
||||||
|
*/
|
||||||
|
int removeOaOrphansWithoutParent();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -125,6 +125,8 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
|
|||||||
CurrencyExcel excel = data.get(index);
|
CurrencyExcel excel = data.get(index);
|
||||||
try {
|
try {
|
||||||
Currency currency = Objects.requireNonNull(BeanUtil.copyProperties(excel, Currency.class));
|
Currency currency = Objects.requireNonNull(BeanUtil.copyProperties(excel, Currency.class));
|
||||||
|
currency.setEffectiveDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEffectiveDate(), "生效日期"));
|
||||||
|
currency.setExpiryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpiryDate(), "失效日期"));
|
||||||
currency.setDataSource(SOURCE_BATCH);
|
currency.setDataSource(SOURCE_BATCH);
|
||||||
currency.setStatus(STATUS_ENABLED);
|
currency.setStatus(STATUS_ENABLED);
|
||||||
prepare(currency, SOURCE_BATCH);
|
prepare(currency, SOURCE_BATCH);
|
||||||
|
|||||||
+284
-29
@@ -46,6 +46,7 @@ import org.springblade.system.cache.SysCache;
|
|||||||
import org.springblade.system.mapper.DeptMapper;
|
import org.springblade.system.mapper.DeptMapper;
|
||||||
import org.springblade.system.pojo.entity.Dept;
|
import org.springblade.system.pojo.entity.Dept;
|
||||||
import org.springblade.system.pojo.entity.User;
|
import org.springblade.system.pojo.entity.User;
|
||||||
|
import org.springblade.system.pojo.enums.DeptCategory;
|
||||||
import org.springblade.system.pojo.vo.DeptVO;
|
import org.springblade.system.pojo.vo.DeptVO;
|
||||||
import org.springblade.system.pojo.vo.UserVO;
|
import org.springblade.system.pojo.vo.UserVO;
|
||||||
import org.springblade.system.props.IamSyncProperties;
|
import org.springblade.system.props.IamSyncProperties;
|
||||||
@@ -53,6 +54,7 @@ import org.springblade.system.service.IDeptService;
|
|||||||
import org.springblade.system.service.IUserService;
|
import org.springblade.system.service.IUserService;
|
||||||
import org.springblade.system.wrapper.DeptWrapper;
|
import org.springblade.system.wrapper.DeptWrapper;
|
||||||
import org.springblade.system.wrapper.UserWrapper;
|
import org.springblade.system.wrapper.UserWrapper;
|
||||||
|
import org.springblade.thirdparty.oa.constant.OAConstant;
|
||||||
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
|
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -232,6 +234,21 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
return removeByIds(idList);
|
return removeByIds(idList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int clearNonTopDepts() {
|
||||||
|
Long nonTopCount = this.count(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.isNotNull(Dept::getParentId)
|
||||||
|
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
|
||||||
|
if (nonTopCount == null || nonTopCount == 0L) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.remove(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.isNotNull(Dept::getParentId)
|
||||||
|
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
|
||||||
|
return nonTopCount.intValue();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public boolean submit(Dept dept) {
|
public boolean submit(Dept dept) {
|
||||||
@@ -451,8 +468,12 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void validateDeptCategory(Dept dept, Dept parent) {
|
private void validateDeptCategory(Dept dept, Dept parent) {
|
||||||
|
// 顶级组织不要求选择上级组织
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
throw new ServiceException("请选择上级组织");
|
if (Integer.valueOf(6).equals(dept.getDeptCategory()) && Func.isEmpty(dept.getCarrierCustomerId())) {
|
||||||
|
throw new ServiceException("请选择承运商");
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
List<Integer> allowedCategories;
|
List<Integer> allowedCategories;
|
||||||
if (BladeConstant.TOP_PARENT_ID.equals(parent.getParentId())) {
|
if (BladeConstant.TOP_PARENT_ID.equals(parent.getParentId())) {
|
||||||
@@ -483,13 +504,24 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
if (deptCode.length() > 30) {
|
if (deptCode.length() > 30) {
|
||||||
throw new ServiceException("组织编码不能超过30个字符");
|
throw new ServiceException("组织编码不能超过30个字符");
|
||||||
}
|
}
|
||||||
if (parent == null || StringUtil.isBlank(parent.getDeptCode())) {
|
// 顶级组织不依赖上级编码
|
||||||
|
if (parent == null) {
|
||||||
|
this.ensureDeptCodeUnique(dept, deptCode);
|
||||||
|
dept.setDeptCode(deptCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtil.isBlank(parent.getDeptCode())) {
|
||||||
throw new ServiceException("请选择已配置组织编码的上级组织");
|
throw new ServiceException("请选择已配置组织编码的上级组织");
|
||||||
}
|
}
|
||||||
String pattern = Pattern.quote(parent.getDeptCode()) + "-\\d+";
|
String pattern = Pattern.quote(parent.getDeptCode()) + "-\\d+";
|
||||||
if (!deptCode.matches(pattern)) {
|
if (!deptCode.matches(pattern)) {
|
||||||
throw new ServiceException("组织编码格式应为:上级编码-分段数字");
|
throw new ServiceException("组织编码格式应为:上级编码-分段数字");
|
||||||
}
|
}
|
||||||
|
this.ensureDeptCodeUnique(dept, deptCode);
|
||||||
|
dept.setDeptCode(deptCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureDeptCodeUnique(Dept dept, String deptCode) {
|
||||||
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery().eq(Dept::getDeptCode, deptCode);
|
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery().eq(Dept::getDeptCode, deptCode);
|
||||||
if (Func.isNotEmpty(dept.getId())) {
|
if (Func.isNotEmpty(dept.getId())) {
|
||||||
queryWrapper.ne(Dept::getId, dept.getId());
|
queryWrapper.ne(Dept::getId, dept.getId());
|
||||||
@@ -497,7 +529,6 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
if (baseMapper.selectCount(queryWrapper) > 0) {
|
if (baseMapper.selectCount(queryWrapper) > 0) {
|
||||||
throw new ServiceException("组织编码已存在");
|
throw new ServiceException("组织编码已存在");
|
||||||
}
|
}
|
||||||
dept.setDeptCode(deptCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -540,24 +571,243 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
return UserWrapper.build().listVO(userList);
|
return UserWrapper.build().listVO(userList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。
|
|
||||||
*/
|
|
||||||
private void applyTenantScope(QueryWrapper<Dept> queryWrapper) {
|
|
||||||
if (!AuthUtil.isAdministrator()) {
|
|
||||||
queryWrapper.lambda().eq(Dept::getTenantId, AuthUtil.getTenantId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
public void updateAncestors(Date startTime) {
|
public void updateAncestors(Date startTime) {
|
||||||
// 1. 查询需要更新祖级列表的部门
|
this.refreshAncestors(false, startTime);
|
||||||
List<Dept> list = this.list(Wrappers.<Dept>lambdaQuery()
|
}
|
||||||
// 祖级列表为空
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public void rebuildOaHierarchy() {
|
||||||
|
List<Dept> oaDeptList = this.list(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.isNotNull(Dept::getOaId)
|
||||||
|
.ne(Dept::getOaId, ""));
|
||||||
|
if (CollectionUtil.isEmpty(oaDeptList)) {
|
||||||
|
log.info("没有记录OA主键的组织,跳过层级重建");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
|
||||||
|
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
|
||||||
|
Map<Long, Long> desiredParentIdMap = new LinkedHashMap<>();
|
||||||
|
Map<Long, String> desiredParentCodeMap = new HashMap<>();
|
||||||
|
for (Dept dept : oaDeptList) {
|
||||||
|
OaParentLink parentLink = this.resolveOaParentLink(dept, companyByOaId, departmentByOaId);
|
||||||
|
if (parentLink == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
desiredParentIdMap.put(dept.getId(), parentLink.parentId());
|
||||||
|
desiredParentCodeMap.put(dept.getId(), parentLink.parentCode());
|
||||||
|
}
|
||||||
|
Set<Long> cyclicIds = this.findCyclicDeptIds(desiredParentIdMap);
|
||||||
|
if (CollectionUtil.isNotEmpty(cyclicIds)) {
|
||||||
|
log.warn("检测到OA组织循环引用,跳过层级重建 ids={}", cyclicIds);
|
||||||
|
cyclicIds.forEach(cyclicId -> {
|
||||||
|
desiredParentIdMap.remove(cyclicId);
|
||||||
|
desiredParentCodeMap.remove(cyclicId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
List<Dept> updateList = new ArrayList<>();
|
||||||
|
for (Dept dept : oaDeptList) {
|
||||||
|
Long parentId = desiredParentIdMap.get(dept.getId());
|
||||||
|
if (parentId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String parentCode = desiredParentCodeMap.get(dept.getId());
|
||||||
|
if (Objects.equals(parentId, dept.getParentId()) && StringUtil.equals(parentCode, dept.getParentCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Dept updateParam = new Dept();
|
||||||
|
updateParam.setId(dept.getId());
|
||||||
|
updateParam.setParentId(parentId);
|
||||||
|
updateParam.setParentCode(parentCode);
|
||||||
|
updateList.add(updateParam);
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||||
|
this.updateBatchById(updateList);
|
||||||
|
log.info("按OA主键重建组织层级,更新父子关系{}条", updateList.size());
|
||||||
|
} else {
|
||||||
|
log.info("OA组织层级已是最新,无需调整父子关系");
|
||||||
|
}
|
||||||
|
this.refreshAncestors(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int removeOaOrphansWithoutParent() {
|
||||||
|
List<Dept> oaDeptList = this.list(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.isNotNull(Dept::getOaId)
|
||||||
|
.ne(Dept::getOaId, ""));
|
||||||
|
if (CollectionUtil.isEmpty(oaDeptList)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
|
||||||
|
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
|
||||||
|
Set<Long> deleteIds = new HashSet<>();
|
||||||
|
for (Dept dept : oaDeptList) {
|
||||||
|
String parentOaId = dept.getOaSupSubComId();
|
||||||
|
// 上级为空或为 0:视为本地锚点/根级,不作为孤儿删除
|
||||||
|
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean parentExists;
|
||||||
|
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
|
||||||
|
parentExists = companyByOaId.containsKey(parentOaId);
|
||||||
|
} else {
|
||||||
|
parentExists = departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
|
||||||
|
}
|
||||||
|
if (!parentExists) {
|
||||||
|
deleteIds.add(dept.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isEmpty(deleteIds)) {
|
||||||
|
log.info("没有需要清理的OA孤儿组织");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 按 OA 上级关系级联:上级被删则子级一并删除
|
||||||
|
boolean oaCascadeChanged = true;
|
||||||
|
while (oaCascadeChanged) {
|
||||||
|
oaCascadeChanged = false;
|
||||||
|
for (Dept dept : oaDeptList) {
|
||||||
|
if (deleteIds.contains(dept.getId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String parentOaId = dept.getOaSupSubComId();
|
||||||
|
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Dept parent = DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())
|
||||||
|
? companyByOaId.get(parentOaId)
|
||||||
|
: Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId));
|
||||||
|
if (parent != null && deleteIds.contains(parent.getId())) {
|
||||||
|
deleteIds.add(dept.getId());
|
||||||
|
oaCascadeChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按本地 parent_id 再级联一轮,清掉挂在已删节点下的子树
|
||||||
|
List<Dept> allDeptList = this.list();
|
||||||
|
boolean parentIdCascadeChanged = true;
|
||||||
|
while (parentIdCascadeChanged) {
|
||||||
|
parentIdCascadeChanged = false;
|
||||||
|
for (Dept dept : allDeptList) {
|
||||||
|
if (deleteIds.contains(dept.getId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dept.getParentId() != null && deleteIds.contains(dept.getParentId())) {
|
||||||
|
deleteIds.add(dept.getId());
|
||||||
|
parentIdCascadeChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.removeByIds(deleteIds);
|
||||||
|
log.info("删除OA层级无法挂载的组织{}条", deleteIds.size());
|
||||||
|
return deleteIds.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按机构类型建立 OA 主键索引
|
||||||
|
*/
|
||||||
|
private Map<String, Dept> indexByOaId(List<Dept> oaDeptList, Integer deptCategory) {
|
||||||
|
Map<String, Dept> oaIdMap = new HashMap<>();
|
||||||
|
for (Dept dept : oaDeptList) {
|
||||||
|
if (!Objects.equals(deptCategory, dept.getDeptCategory()) || StringUtil.isBlank(dept.getOaId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Dept previous = oaIdMap.put(dept.getOaId(), dept);
|
||||||
|
if (previous != null) {
|
||||||
|
log.warn("OA主键重复,层级重建使用后一条 category={} oaId={} id={} previousId={}",
|
||||||
|
dept.getDeptCategory(), dept.getOaId(), dept.getId(), previous.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oaIdMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 OA 上级主键解析本系统父级
|
||||||
|
*/
|
||||||
|
private OaParentLink resolveOaParentLink(Dept dept, Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
|
||||||
|
String parentOaId = dept.getOaSupSubComId();
|
||||||
|
boolean rootParent = StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId);
|
||||||
|
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
|
||||||
|
if (rootParent) {
|
||||||
|
return new OaParentLink(OAConvertConstant.ROOT_PARENT_ID, OAConstant.ROOT_COMPANY_ID);
|
||||||
|
}
|
||||||
|
Dept parent = companyByOaId.get(parentOaId);
|
||||||
|
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
|
||||||
|
log.warn("未找到上级公司,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
|
||||||
|
dept.getOaId(), parentOaId, dept.getId());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, OAConvertConstant.COMPANY_OA_PREFIX, parentOaId));
|
||||||
|
}
|
||||||
|
if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (rootParent) {
|
||||||
|
log.warn("部门缺少上级OA主键,跳过层级重建 oaId={} deptId={}", dept.getOaId(), dept.getId());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean parentIsDepartment = StringUtil.isNotBlank(dept.getParentCode())
|
||||||
|
&& dept.getParentCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
|
||||||
|
boolean parentIsCompany = StringUtil.isNotBlank(dept.getParentCode())
|
||||||
|
&& dept.getParentCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX);
|
||||||
|
Dept parent;
|
||||||
|
if (parentIsDepartment) {
|
||||||
|
parent = departmentByOaId.get(parentOaId);
|
||||||
|
} else if (parentIsCompany) {
|
||||||
|
parent = companyByOaId.get(parentOaId);
|
||||||
|
} else {
|
||||||
|
parent = departmentByOaId.get(parentOaId);
|
||||||
|
if (parent == null) {
|
||||||
|
parent = companyByOaId.get(parentOaId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
|
||||||
|
log.warn("未找到上级组织,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
|
||||||
|
dept.getOaId(), parentOaId, dept.getId());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String prefix = DeptCategory.DEPT.getCode().equals(parent.getDeptCategory())
|
||||||
|
? OAConvertConstant.DEPARTMENT_OA_PREFIX : OAConvertConstant.COMPANY_OA_PREFIX;
|
||||||
|
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, prefix, parentOaId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveParentCode(Dept parent, String prefix, String parentOaId) {
|
||||||
|
if (StringUtil.isNotBlank(parent.getDeptCode())) {
|
||||||
|
return parent.getDeptCode();
|
||||||
|
}
|
||||||
|
return prefix + parentOaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 找出父子关系成环的部门
|
||||||
|
*/
|
||||||
|
private Set<Long> findCyclicDeptIds(Map<Long, Long> parentIdMap) {
|
||||||
|
Set<Long> cyclicIds = new HashSet<>();
|
||||||
|
for (Long deptId : parentIdMap.keySet()) {
|
||||||
|
Set<Long> visiting = new HashSet<>();
|
||||||
|
Long currentId = deptId;
|
||||||
|
while (currentId != null && !OAConvertConstant.ROOT_PARENT_ID.equals(currentId)) {
|
||||||
|
if (!visiting.add(currentId)) {
|
||||||
|
cyclicIds.addAll(visiting);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentId = parentIdMap.get(currentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cyclicIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新祖级列表
|
||||||
|
*
|
||||||
|
* @param refreshAll 是否刷新全部部门
|
||||||
|
* @param startTime 增量起始时间,仅 refreshAll 为 false 时生效
|
||||||
|
*/
|
||||||
|
private void refreshAncestors(boolean refreshAll, Date startTime) {
|
||||||
|
List<Dept> list = refreshAll ? this.list() : this.list(Wrappers.<Dept>lambdaQuery()
|
||||||
.isNull(Dept::getAncestors)
|
.isNull(Dept::getAncestors)
|
||||||
// 开始时间不为空,查询同步时间大于开始时间或修改时间大于开始时间的数据
|
|
||||||
.or(startTime != null, wrapper -> wrapper.ge(Dept::getSyncTime, startTime))
|
.or(startTime != null, wrapper -> wrapper.ge(Dept::getSyncTime, startTime))
|
||||||
.or(startTime != null, wrapper -> wrapper.ge(Dept::getUpdateTime, startTime))
|
.or(startTime != null, wrapper -> wrapper.ge(Dept::getUpdateTime, startTime))
|
||||||
);
|
);
|
||||||
@@ -565,29 +815,19 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
log.info("需要更新祖级列表的部门为空");
|
log.info("需要更新祖级列表的部门为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 所有公司部门列表
|
List<DeptVO> allList = (refreshAll ? list : this.list()).stream()
|
||||||
List<DeptVO> allList = this.list().stream()
|
|
||||||
.map(dept -> BeanUtil.copyProperties(dept, DeptVO.class))
|
.map(dept -> BeanUtil.copyProperties(dept, DeptVO.class))
|
||||||
.toList();
|
.toList();
|
||||||
// id对应的部门map
|
|
||||||
Map<Long, DeptVO> idDeptMap = allList.stream()
|
Map<Long, DeptVO> idDeptMap = allList.stream()
|
||||||
.collect(Collectors.toMap(DeptVO::getId, Function.identity(), (a, b) -> b));
|
.collect(Collectors.toMap(DeptVO::getId, Function.identity(), (a, b) -> b));
|
||||||
// 获取根节点列表
|
|
||||||
List<DeptVO> rootList = getRootList(allList, idDeptMap);
|
List<DeptVO> rootList = getRootList(allList, idDeptMap);
|
||||||
// 计算祖级列表
|
|
||||||
setAncestors(rootList);
|
setAncestors(rootList);
|
||||||
|
|
||||||
// 3. 更新祖级列表
|
|
||||||
List<Dept> updateParams = list.stream()
|
List<Dept> updateParams = list.stream()
|
||||||
.filter(dept -> {
|
.filter(dept -> {
|
||||||
DeptVO vo = idDeptMap.get(dept.getId());
|
DeptVO vo = idDeptMap.get(dept.getId());
|
||||||
if (vo == null) {
|
if (vo == null || vo.getAncestors() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (vo.getAncestors() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 祖级列表和数据库的不一样的才需要更新
|
|
||||||
return !StringUtil.equals(vo.getAncestors(), dept.getAncestors());
|
return !StringUtil.equals(vo.getAncestors(), dept.getAncestors());
|
||||||
})
|
})
|
||||||
.map(dept -> {
|
.map(dept -> {
|
||||||
@@ -602,6 +842,21 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OA 上级链接
|
||||||
|
*/
|
||||||
|
private record OaParentLink(Long parentId, String parentCode) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的租户范围过滤:超管放行,其他用户强制限定为当前会话租户。
|
||||||
|
*/
|
||||||
|
private void applyTenantScope(QueryWrapper<Dept> queryWrapper) {
|
||||||
|
if (!AuthUtil.isAdministrator()) {
|
||||||
|
queryWrapper.lambda().eq(Dept::getTenantId, AuthUtil.getTenantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取根节点列表
|
* 获取根节点列表
|
||||||
* @param allList
|
* @param allList
|
||||||
|
|||||||
+116
-46
@@ -74,10 +74,18 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
Date startTime = this.syncAndRecord(DataSyncRecordUtils::createOAOrgFetch, queryStartTime -> {
|
Date startTime = this.syncAndRecord(DataSyncRecordUtils::createOAOrgFetch, queryStartTime -> {
|
||||||
// 1. 同步公司
|
// 1. 同步公司
|
||||||
this.syncCompany(queryStartTime);
|
this.syncCompany(queryStartTime);
|
||||||
|
// 公司分批入库后,重建层级并清理无法挂载的公司
|
||||||
|
deptService.rebuildOaHierarchy();
|
||||||
|
deptService.removeOaOrphansWithoutParent();
|
||||||
// 2. 同步部门
|
// 2. 同步部门
|
||||||
this.syncDept(queryStartTime);
|
this.syncDept(queryStartTime);
|
||||||
// 2. 更新部门祖级列表
|
// 3. 分批写入完成后,按 OA 主键重建组织、部门层级
|
||||||
|
deptService.rebuildOaHierarchy();
|
||||||
|
deptService.removeOaOrphansWithoutParent();
|
||||||
|
// 4. 更新部门祖级列表
|
||||||
deptService.updateAncestors(queryStartTime);
|
deptService.updateAncestors(queryStartTime);
|
||||||
|
CacheUtil.clear(SYS_CACHE);
|
||||||
|
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||||
}, syncAll);
|
}, syncAll);
|
||||||
// 2. 推送mk
|
// 2. 推送mk
|
||||||
mkPushService.pushOrgAndRecord(startTime);
|
mkPushService.pushOrgAndRecord(startTime);
|
||||||
@@ -196,7 +204,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
return new ServiceException("调用OA接口查询公司信息失败");
|
return new ServiceException("调用OA接口查询公司信息失败");
|
||||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||||
// 处理数据
|
// 处理数据
|
||||||
OrgSyncCount syncCount = handleCompany(list);
|
OrgSyncCount syncCount = handleCompany(list, false);
|
||||||
notHandleList.addAll(syncCount.getNotHandledList());
|
notHandleList.addAll(syncCount.getNotHandledList());
|
||||||
});
|
});
|
||||||
// 3. 未处理的数据
|
// 3. 未处理的数据
|
||||||
@@ -213,7 +221,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
* @param startTime 查询开始时间
|
* @param startTime 查询开始时间
|
||||||
*/
|
*/
|
||||||
private void syncDept(Date startTime) {
|
private void syncDept(Date startTime) {
|
||||||
String subCompanyIds = getSubCompanyIds();
|
String subCompanyIds = getOaSubCompanyIds(false);
|
||||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||||
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
|
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
|
||||||
return;
|
return;
|
||||||
@@ -228,7 +236,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
return new ServiceException("调用OA接口查询部门信息失败");
|
return new ServiceException("调用OA接口查询部门信息失败");
|
||||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||||
// 处理数据
|
// 处理数据
|
||||||
OrgSyncCount syncCount = handleDept(list);
|
OrgSyncCount syncCount = handleDept(list, false);
|
||||||
notHandleList.addAll(syncCount.getNotHandledList());
|
notHandleList.addAll(syncCount.getNotHandledList());
|
||||||
});
|
});
|
||||||
// 3. 未处理的数据
|
// 3. 未处理的数据
|
||||||
@@ -334,13 +342,18 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
List<OACompanyResponse> oaCompanies = responseData.getDataList() == null
|
List<OACompanyResponse> oaCompanies = responseData.getDataList() == null
|
||||||
? Collections.emptyList() : responseData.getDataList();
|
? Collections.emptyList() : responseData.getDataList();
|
||||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||||
OrgSyncCount syncCount = handleCompany(oaCompanies);
|
OrgSyncCount syncCount = handleCompany(oaCompanies, true);
|
||||||
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
||||||
ComposeLogUtil.getLastLog().warn("同步公司,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
|
ComposeLogUtil.getLastLog().warn("同步公司,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
|
||||||
}
|
}
|
||||||
|
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("company", pageNo, pageSize, totalSize, oaCompanies.size(), syncCount);
|
||||||
|
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
||||||
|
// 公司全部分批入库后:重建层级,并删除上级不存在的数据
|
||||||
|
deptService.rebuildOaHierarchy();
|
||||||
|
deptService.removeOaOrphansWithoutParent();
|
||||||
|
}
|
||||||
CacheUtil.clear(SYS_CACHE);
|
CacheUtil.clear(SYS_CACHE);
|
||||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||||
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("company", pageNo, pageSize, totalSize, oaCompanies.size(), syncCount);
|
|
||||||
ComposeLogUtil.getLastLog().info("OA公司分页同步完成 {}/{},成功{},跳过{}",
|
ComposeLogUtil.getLastLog().info("OA公司分页同步完成 {}/{},成功{},跳过{}",
|
||||||
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||||
return pageVO;
|
return pageVO;
|
||||||
@@ -356,9 +369,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size) {
|
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size) {
|
||||||
int pageNo = current < 1 ? 1 : current;
|
int pageNo = current < 1 ? 1 : current;
|
||||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||||
String subCompanyIds = getSubCompanyIds();
|
String subCompanyIds = getOaSubCompanyIds(true);
|
||||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||||
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数,跳过部门同步");
|
ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步");
|
||||||
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
|
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
|
||||||
emptyPageVO.setFinished(true);
|
emptyPageVO.setFinished(true);
|
||||||
return emptyPageVO;
|
return emptyPageVO;
|
||||||
@@ -375,17 +388,19 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
List<OADepartmentResponse> oaDepartments = responseData.getDataList() == null
|
List<OADepartmentResponse> oaDepartments = responseData.getDataList() == null
|
||||||
? Collections.emptyList() : responseData.getDataList();
|
? Collections.emptyList() : responseData.getDataList();
|
||||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||||
OrgSyncCount syncCount = handleDept(oaDepartments);
|
OrgSyncCount syncCount = handleDept(oaDepartments, true);
|
||||||
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
||||||
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(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);
|
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
|
||||||
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
||||||
// 部门同步完成后更新祖级列表
|
// 部门分批同步完成后,重建层级、清理无法挂载数据并刷新祖级列表
|
||||||
|
deptService.rebuildOaHierarchy();
|
||||||
|
deptService.removeOaOrphansWithoutParent();
|
||||||
deptService.updateAncestors(null);
|
deptService.updateAncestors(null);
|
||||||
}
|
}
|
||||||
|
CacheUtil.clear(SYS_CACHE);
|
||||||
|
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||||
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 {}/{},成功{},跳过{}",
|
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 {}/{},成功{},跳过{}",
|
||||||
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||||
return pageVO;
|
return pageVO;
|
||||||
@@ -469,19 +484,22 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理oa公司
|
* 处理oa公司
|
||||||
* @param oaCompanies
|
* @param oaCompanies OA公司列表
|
||||||
|
* @param syncAll true=自动同步时不过滤字典白名单;false=仅同步配置的公司名称
|
||||||
* @return 同步统计
|
* @return 同步统计
|
||||||
*/
|
*/
|
||||||
private OrgSyncCount handleCompany(List<OACompanyResponse> oaCompanies) {
|
private OrgSyncCount handleCompany(List<OACompanyResponse> oaCompanies, boolean syncAll) {
|
||||||
if (CollectionUtil.isEmpty(oaCompanies)) {
|
if (CollectionUtil.isEmpty(oaCompanies)) {
|
||||||
return OrgSyncCount.empty();
|
return OrgSyncCount.empty();
|
||||||
}
|
}
|
||||||
// 获取需要的公司名称
|
// 获取需要的公司名称
|
||||||
Set<String> companyNames = getCompanyNames();
|
Set<String> companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
|
||||||
// 转换数据
|
// 转换数据
|
||||||
List<Dept> allParam = oaCompanies.stream()
|
List<Dept> allParam = oaCompanies.stream()
|
||||||
// 筛选只要需要的公司
|
// 强制跳过上级公司为 0 的数据
|
||||||
.filter(company -> companyNames.contains(company.getSubcompanyname()))
|
.filter(company -> !OAConstant.ROOT_COMPANY_ID.equals(company.getSupsubcomid()))
|
||||||
|
// 非全量同步时,筛选只要配置的公司
|
||||||
|
.filter(company -> syncAll || companyNames.contains(company.getSubcompanyname()))
|
||||||
.map(deptConvert::company2dept)
|
.map(deptConvert::company2dept)
|
||||||
.toList();
|
.toList();
|
||||||
int filteredSkipCount = oaCompanies.size() - allParam.size();
|
int filteredSkipCount = oaCompanies.size() - allParam.size();
|
||||||
@@ -501,10 +519,11 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理oa部门
|
* 处理oa部门
|
||||||
* @param oaDepts
|
* @param oaDepts OA部门列表
|
||||||
|
* @param syncAll true=自动同步时不过滤根公司部门白名单;false=沿用原过滤规则
|
||||||
* @return 同步统计
|
* @return 同步统计
|
||||||
*/
|
*/
|
||||||
private OrgSyncCount handleDept(List<OADepartmentResponse> oaDepts) {
|
private OrgSyncCount handleDept(List<OADepartmentResponse> oaDepts, boolean syncAll) {
|
||||||
if (CollectionUtil.isEmpty(oaDepts)) {
|
if (CollectionUtil.isEmpty(oaDepts)) {
|
||||||
return OrgSyncCount.empty();
|
return OrgSyncCount.empty();
|
||||||
}
|
}
|
||||||
@@ -512,31 +531,42 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
Map<String, Long> companyDeptMap = getAllCompanyDeptMap();
|
Map<String, Long> companyDeptMap = getAllCompanyDeptMap();
|
||||||
// 根公司id
|
// 根公司id
|
||||||
String rootCompanyId = getRootCompanyId();
|
String rootCompanyId = getRootCompanyId();
|
||||||
if (rootCompanyId == null) {
|
if (!syncAll && rootCompanyId == null) {
|
||||||
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
|
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
|
||||||
}
|
}
|
||||||
// 根公司下要同步的部门名称
|
// 根公司下要同步的部门名称
|
||||||
Set<String> rootCompanyDeptNames = getRootCompanyDeptNames();
|
Set<String> rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
|
||||||
// 转换数据
|
// 转换数据
|
||||||
List<Dept> allParam = oaDepts.stream()
|
List<Dept> allParam = oaDepts.stream()
|
||||||
// 部门的公司不是根公司,或者部门名称在根公司下需要同步的部门列表中,且是根部门
|
// 全量同步时不过滤;否则:部门的公司不是根公司,或者部门名称在根公司下需要同步的部门列表中,且是根部门
|
||||||
.filter(oaDept -> !rootCompanyId.equals(oaDept.getSubcompanyid1()) || (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
|
.filter(oaDept -> syncAll
|
||||||
|
|| rootCompanyId == null
|
||||||
|
|| !rootCompanyId.equals(oaDept.getSubcompanyid1())
|
||||||
|
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
|
||||||
.map(dept -> deptConvert.dept2dept(dept, companyDeptMap))
|
.map(dept -> deptConvert.dept2dept(dept, companyDeptMap))
|
||||||
.toList();
|
.toList();
|
||||||
int filteredSkipCount = oaDepts.size() - allParam.size();
|
int filteredSkipCount = oaDepts.size() - allParam.size();
|
||||||
if (CollectionUtil.isEmpty(allParam)) {
|
if (CollectionUtil.isEmpty(allParam)) {
|
||||||
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
|
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
|
||||||
}
|
}
|
||||||
Set<String> deptCodes = allParam.stream()
|
Set<String> deptCodes = new HashSet<>();
|
||||||
.map(Dept::getDeptCode)
|
allParam.forEach(dept -> {
|
||||||
.collect(Collectors.toSet());
|
if (StringUtils.isNotEmpty(dept.getDeptCode())) {
|
||||||
// 查询数据库的部门,转换成map
|
deptCodes.add(dept.getDeptCode());
|
||||||
Map<String, Long> deptMap = deptService.list(Wrappers.<Dept>lambdaQuery()
|
}
|
||||||
|
if (StringUtils.isNotEmpty(dept.getParentCode())) {
|
||||||
|
deptCodes.add(dept.getParentCode());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 查询数据库的部门,转换成map。包含本页及上级编码,便于命中已入库的上级
|
||||||
|
Map<String, Long> deptMap = deptCodes.isEmpty() ? new HashMap<>() : deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||||
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||||
.in(Dept::getDeptCode, deptCodes)
|
.in(Dept::getDeptCode, deptCodes)
|
||||||
).stream()
|
).stream()
|
||||||
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
|
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
|
||||||
.collect(Collectors.toMap(Dept::getDeptCode, Dept::getId, (a, b) -> b));
|
.collect(Collectors.toMap(Dept::getDeptCode, Dept::getId, (a, b) -> b));
|
||||||
|
// 公司编码也放入 map,根部门挂到公司时才能解析父级
|
||||||
|
companyDeptMap.forEach(deptMap::putIfAbsent);
|
||||||
// 处理部门
|
// 处理部门
|
||||||
List<Dept> notHandledList = this.handleDept(allParam, deptMap, DeptCategory.DEPT);
|
List<Dept> notHandledList = this.handleDept(allParam, deptMap, DeptCategory.DEPT);
|
||||||
int syncedCount = allParam.size() - notHandledList.size();
|
int syncedCount = allParam.size() - notHandledList.size();
|
||||||
@@ -564,14 +594,30 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重新把没有父id的数据设置一下父id
|
// 重新把没有父id的数据设置一下父id;上级尚未入库时,新增记录先挂到根节点,全部分批完成后重建层级并清理无法挂载的数据
|
||||||
allParam.stream()
|
int unresolvedParentCount = 0;
|
||||||
.filter(dept -> dept.getParentId() == null && deptMap.containsKey(dept.getDeptCode()))
|
for (Dept dept : allParam) {
|
||||||
.forEach(dept -> dept.setParentId(deptMap.get(dept.getParentCode())));
|
if (dept.getParentId() != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Long parentId = deptMap.get(dept.getParentCode());
|
||||||
|
if (parentId != null) {
|
||||||
|
dept.setParentId(parentId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unresolvedParentCount++;
|
||||||
|
if (!existsDeptIds.contains(dept.getId())) {
|
||||||
|
dept.setParentId(OAConvertConstant.ROOT_PARENT_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unresolvedParentCount > 0) {
|
||||||
|
ComposeLogUtil.getLastLog().warn("同步{}时有{}条上级尚未入库,已先保存,分批结束后重建层级并清理无法挂载数据",
|
||||||
|
deptCategory.getName(), unresolvedParentCount);
|
||||||
|
}
|
||||||
|
|
||||||
// 父id不为空,保存数据
|
// 父id不为空才新增;已存在的记录即使父id未解析也更新(空父id不会覆盖原值)
|
||||||
List<Dept> list = allParam.stream()
|
List<Dept> list = allParam.stream()
|
||||||
.filter(dept -> dept.getParentId() != null)
|
.filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
|
||||||
.toList();
|
.toList();
|
||||||
// 不存在的新增
|
// 不存在的新增
|
||||||
List<Dept> addList = list.stream()
|
List<Dept> addList = list.stream()
|
||||||
@@ -589,9 +635,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
|
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
|
||||||
deptService.updateBatchById(updateList);
|
deptService.updateBatchById(updateList);
|
||||||
}
|
}
|
||||||
// 返回未处理的
|
// 返回仍未保存的数据
|
||||||
return allParam.stream()
|
return allParam.stream()
|
||||||
.filter(dept -> dept.getParentId() == null)
|
.filter(dept -> dept.getParentId() == null && !existsDeptIds.contains(dept.getId()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,26 +673,50 @@ public class OASyncServiceImpl implements IOASyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取oa查询参数,子公司id参数
|
* 获取 OA 部门查询使用的子公司 id
|
||||||
* @return
|
*
|
||||||
|
* @param syncAll true=取已同步的全部 OA 公司;false=仅取字典配置的公司
|
||||||
|
* @return 逗号分隔的 OA 公司 id
|
||||||
*/
|
*/
|
||||||
private String getSubCompanyIds() {
|
private String getOaSubCompanyIds(boolean syncAll) {
|
||||||
|
List<Dept> companyList;
|
||||||
|
if (syncAll) {
|
||||||
|
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
Set<String> companyNames = this.getCompanyNames();
|
Set<String> companyNames = this.getCompanyNames();
|
||||||
if (companyNames.isEmpty()) {
|
if (companyNames.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return deptService.list(Wrappers.<Dept>lambdaQuery()
|
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||||
// 根据名称查询公司
|
|
||||||
.in(Dept::getDeptName, companyNames)
|
.in(Dept::getDeptName, companyNames)
|
||||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||||
).stream()
|
);
|
||||||
// 只要部门编码不为空,且包含了oa公司前缀的
|
}
|
||||||
.filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().contains(OAConvertConstant.COMPANY_OA_PREFIX))
|
return companyList.stream()
|
||||||
// 去掉oa公司前缀
|
.map(this::resolveOaCompanyId)
|
||||||
.map(dept -> dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, ""))
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.distinct()
|
||||||
.collect(Collectors.joining(","));
|
.collect(Collectors.joining(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析本地公司对应的 OA 公司 id
|
||||||
|
*/
|
||||||
|
private String resolveOaCompanyId(Dept dept) {
|
||||||
|
if (dept == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(dept.getOaId())) {
|
||||||
|
return dept.getOaId();
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX)) {
|
||||||
|
return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取默认角色id
|
* 获取默认角色id
|
||||||
*/
|
*/
|
||||||
|
|||||||
+241
-295
@@ -35,6 +35,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
|
|||||||
import org.springblade.core.tool.utils.BeanUtil;
|
import org.springblade.core.tool.utils.BeanUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springblade.system.cache.UserCache;
|
import org.springblade.system.cache.UserCache;
|
||||||
|
import org.springblade.system.excel.ImportFailureException;
|
||||||
import org.springblade.system.excel.PortTerminalExcel;
|
import org.springblade.system.excel.PortTerminalExcel;
|
||||||
import org.springblade.system.excel.PortTerminalExportExcel;
|
import org.springblade.system.excel.PortTerminalExportExcel;
|
||||||
import org.springblade.system.mapper.PortTerminalMapper;
|
import org.springblade.system.mapper.PortTerminalMapper;
|
||||||
@@ -45,17 +46,19 @@ import org.springblade.system.service.IPortTerminalService;
|
|||||||
import org.springblade.system.service.IRegionService;
|
import org.springblade.system.service.IRegionService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.TreeMap;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,12 +72,10 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
|
|
||||||
private static final String CATEGORY_PORT = "港口";
|
private static final String CATEGORY_PORT = "港口";
|
||||||
private static final String CATEGORY_TERMINAL = "码头";
|
private static final String CATEGORY_TERMINAL = "码头";
|
||||||
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_INITIAL_OLD = "初始导入";
|
||||||
private static final String SOURCE_BATCH = "批量导入";
|
private static final String SOURCE_BATCH = "批量导入";
|
||||||
private static final String SOURCE_MANUAL = "手动录入";
|
private static final String SOURCE_MANUAL = "手工导入";
|
||||||
private static final String SOURCE_MANUAL_OLD = "手工导入";
|
|
||||||
private static final int STATUS_ENABLED = 1;
|
private static final int STATUS_ENABLED = 1;
|
||||||
private static final int STATUS_DISABLED = 2;
|
private static final int STATUS_DISABLED = 2;
|
||||||
private static final int CODE_MAX_LENGTH = 30;
|
private static final int CODE_MAX_LENGTH = 30;
|
||||||
@@ -141,270 +142,209 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
if (Func.isEmpty(data)) {
|
if (Func.isEmpty(data)) {
|
||||||
throw new ServiceException("导入数据不能为空");
|
throw new ServiceException("导入数据不能为空");
|
||||||
}
|
}
|
||||||
Map<Integer, PortTerminalExcel> errorMap = new TreeMap<>();
|
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||||
Map<String, Integer> codeCountMap = buildImportCodeCountMap(data);
|
List<PortTerminalExcel> failureList = new ArrayList<>();
|
||||||
importPortTerminalByCategory(data, codeCountMap, errorMap, true);
|
// 批内已占用的编码,用于识别文件内重复数据。
|
||||||
importPortTerminalByCategory(data, codeCountMap, errorMap, false);
|
Map<String, Integer> occupiedCodeMap = new HashMap<>();
|
||||||
if (Func.isNotEmpty(errorMap)) {
|
// 本文件声明的一级数据(港口)编码集合。
|
||||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
// 用于区分两种情况:父港口"漏填"与"父行自身校验失败"。
|
||||||
|
// 后者说明父行已被标红、用户只需改那一行,因此不再连带给码头行报错。
|
||||||
|
Set<String> declaredPortCodeSet = new HashSet<>();
|
||||||
|
// 两阶段导入:先落库一级数据(港口),再落库二级数据(码头),消除对 Excel 行序的依赖。
|
||||||
|
Map<String, PortTerminal> batchPortMap = new LinkedHashMap<>();
|
||||||
|
List<Integer> portIndexList = new ArrayList<>();
|
||||||
|
List<Integer> terminalIndexList = new ArrayList<>();
|
||||||
|
splitByCategory(data, portIndexList, terminalIndexList, occupiedCodeMap, declaredPortCodeSet, failureList);
|
||||||
|
importPorts(data, portIndexList, batchPortMap, failureList);
|
||||||
|
importTerminals(data, terminalIndexList, batchPortMap, declaredPortCodeSet, failureList);
|
||||||
|
if (Func.isNotEmpty(failureList)) {
|
||||||
|
// 抛出携带失败明细的异常,触发事务回滚。
|
||||||
|
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
|
||||||
|
throw new ImportFailureException(data);
|
||||||
}
|
}
|
||||||
return new ArrayList<>(errorMap.values());
|
return failureList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void importPortTerminalByCategory(List<PortTerminalExcel> data, Map<String, Integer> codeCountMap,
|
/**
|
||||||
Map<Integer, PortTerminalExcel> errorMap, boolean importPort) {
|
* 按类型拆分数据行,并完成与阶段无关的基础校验(类型、编码格式、批内重复)。
|
||||||
|
*
|
||||||
|
* @param data 导入数据
|
||||||
|
* @param portIndexList 港口行下标
|
||||||
|
* @param terminalIndexList 码头行下标
|
||||||
|
* @param occupiedCodeMap 编码占用情况,值为首次出现的下标
|
||||||
|
* @param declaredPortCodeSet 本文件声明的港口编码(含编码格式不合法的行)
|
||||||
|
* @param failureList 失败明细
|
||||||
|
*/
|
||||||
|
private void splitByCategory(List<PortTerminalExcel> data, List<Integer> portIndexList, List<Integer> terminalIndexList,
|
||||||
|
Map<String, Integer> occupiedCodeMap, Set<String> declaredPortCodeSet,
|
||||||
|
List<PortTerminalExcel> failureList) {
|
||||||
for (int index = 0; index < data.size(); index++) {
|
for (int index = 0; index < data.size(); index++) {
|
||||||
PortTerminalExcel excel = data.get(index);
|
PortTerminalExcel excel = data.get(index);
|
||||||
boolean isPort = CATEGORY_PORT.equals(trimToEmpty(excel.getCategory()));
|
String category = trimToEmpty(excel.getCategory());
|
||||||
if (isPort != importPort) {
|
boolean categoryValid = CATEGORY_PORT.equals(category) || CATEGORY_TERMINAL.equals(category);
|
||||||
|
if (!categoryValid) {
|
||||||
|
failureList.add(buildFailure(data, index, "类型只能为港口或码头"));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PortTerminal portTerminal = buildImportPortTerminal(excel);
|
// 码头行的「港口编码」既是编号前缀,也是"所属港口"的来源(见 importTerminals)。
|
||||||
|
// 缺失时下游解析不到父港口,而该异常又会被"父行已声明"的抑制逻辑吞掉,
|
||||||
|
// 导致码头被静默丢弃、接口却返回成功——所以必须在这里明确拦下。
|
||||||
|
if (CATEGORY_TERMINAL.equals(category) && Func.isEmpty(trimToEmpty(excel.getPortCode()))) {
|
||||||
|
failureList.add(buildFailure(data, index, "码头必须填写所属港口编码"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 只要类型是港口就登记为"本文件已声明",即使它的编码格式不合法:
|
||||||
|
// 这样引用它的码头行不会被连带报错,用户只需修正这一个港口行。
|
||||||
|
if (CATEGORY_PORT.equals(category)) {
|
||||||
|
String declaredCode = resolveImportCode(excel);
|
||||||
|
if (Func.isNotEmpty(declaredCode)) {
|
||||||
|
declaredPortCodeSet.add(declaredCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String code = resolveImportCode(excel);
|
||||||
|
boolean codeValid = CATEGORY_PORT.equals(category)
|
||||||
|
? PORT_CODE_PATTERN.matcher(code).matches()
|
||||||
|
: TERMINAL_CODE_PATTERN.matcher(code).matches();
|
||||||
|
if (!codeValid) {
|
||||||
|
String message = CATEGORY_PORT.equals(category) ? "港口编码为5位大写字母" : "码头编码格式为港口编码-码头标识";
|
||||||
|
failureList.add(buildFailure(data, index, message));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 批内重复:两行都需要标记,由用户决定保留哪一行。
|
||||||
|
if (occupiedCodeMap.containsKey(code)) {
|
||||||
|
markFailure(data, occupiedCodeMap.get(code), "编码 " + code + " 在文件中重复出现");
|
||||||
|
failureList.add(buildFailure(data, index, "编码 " + code + " 在文件中重复出现"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
occupiedCodeMap.put(code, index);
|
||||||
|
if (CATEGORY_PORT.equals(category)) {
|
||||||
|
portIndexList.add(index);
|
||||||
|
} else {
|
||||||
|
terminalIndexList.add(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阶段一:导入港口(一级数据),同时登记到批次内存映射,供码头引用。
|
||||||
|
*/
|
||||||
|
private void importPorts(List<PortTerminalExcel> data, List<Integer> portIndexList, Map<String, PortTerminal> batchPortMap,
|
||||||
|
List<PortTerminalExcel> failureList) {
|
||||||
|
for (Integer index : portIndexList) {
|
||||||
|
PortTerminalExcel excel = data.get(index);
|
||||||
|
try {
|
||||||
|
PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class));
|
||||||
|
// 导入模板按"港口编码/码头编码"两列填写,需归并为实体编码。
|
||||||
|
portTerminal.setCode(resolveImportCode(excel));
|
||||||
portTerminal.setDataSource(SOURCE_BATCH);
|
portTerminal.setDataSource(SOURCE_BATCH);
|
||||||
portTerminal.setStatus(STATUS_ENABLED);
|
portTerminal.setStatus(STATUS_ENABLED);
|
||||||
normalizeImportPortTerminal(portTerminal);
|
|
||||||
List<String> validationErrors = validateImportPortTerminal(excel, portTerminal, codeCountMap);
|
|
||||||
if (Func.isNotEmpty(validationErrors)) {
|
|
||||||
excel.setErrorMessage(formatImportErrorMessage(validationErrors));
|
|
||||||
errorMap.put(index, excel);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
prepare(portTerminal, SOURCE_BATCH);
|
prepare(portTerminal, SOURCE_BATCH);
|
||||||
validate(portTerminal);
|
validate(portTerminal);
|
||||||
prepareImportTarget(portTerminal);
|
save(portTerminal);
|
||||||
if (!saveOrUpdate(portTerminal)) {
|
batchPortMap.put(portTerminal.getCode(), portTerminal);
|
||||||
throw new ServiceException("港口码头保存失败");
|
} catch (Exception exception) {
|
||||||
|
failureList.add(buildFailure(data, index, resolveMessage(exception)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阶段二:导入码头(二级数据),上级港口优先取本批次新增的港口,其次回查数据库。
|
||||||
|
*
|
||||||
|
* @param declaredPortCodeSet 本文件声明的港口编码,用于避免连带误报
|
||||||
|
*/
|
||||||
|
private void importTerminals(List<PortTerminalExcel> data, List<Integer> terminalIndexList, Map<String, PortTerminal> batchPortMap,
|
||||||
|
Set<String> declaredPortCodeSet, List<PortTerminalExcel> failureList) {
|
||||||
|
for (Integer index : terminalIndexList) {
|
||||||
|
PortTerminalExcel excel = data.get(index);
|
||||||
|
try {
|
||||||
|
PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class));
|
||||||
|
// 导入模板按"港口编码/码头编码"两列填写,需归并为实体编码;上级港口取本行的港口编码列。
|
||||||
|
portTerminal.setCode(resolveImportCode(excel));
|
||||||
|
portTerminal.setParentCode(trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT));
|
||||||
|
portTerminal.setDataSource(SOURCE_BATCH);
|
||||||
|
portTerminal.setStatus(STATUS_ENABLED);
|
||||||
|
prepareTerminal(portTerminal, batchPortMap);
|
||||||
|
validate(portTerminal);
|
||||||
|
save(portTerminal);
|
||||||
|
} catch (ParentPortNotFoundException exception) {
|
||||||
|
// 上级港口就声明在本文件里,只是那一行自己校验失败(已被标红)。
|
||||||
|
// 此时码头行本身没有问题,不再连带报错,避免用户看到"两行都错"的假象。
|
||||||
|
// 注意这里必须与上面 setParentCode 取同一列(港口编码);
|
||||||
|
// 若改用「上级港口编码」,父港口取不到时会被误判为"已声明"而吞掉异常。
|
||||||
|
if (!declaredPortCodeSet.contains(trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT))) {
|
||||||
|
failureList.add(buildFailure(data, index, resolveMessage(exception)));
|
||||||
}
|
}
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
failureList.add(buildFailure(data, index, resolveMessage(exception)));
|
||||||
excel.setErrorMessage(formatImportErrorMessage(List.of(message)));
|
|
||||||
errorMap.put(index, excel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareImportTarget(PortTerminal portTerminal) {
|
/**
|
||||||
if (Func.isNotEmpty(portTerminal.getId())) {
|
* 码头导入:上级港口优先匹配本批次新增的港口,其次由 prepare 回查数据库。
|
||||||
|
*/
|
||||||
|
private void prepareTerminal(PortTerminal portTerminal, Map<String, PortTerminal> batchPortMap) {
|
||||||
|
String parentCode = trimToEmpty(portTerminal.getParentCode()).toUpperCase(Locale.ROOT);
|
||||||
|
PortTerminal parent = Func.isEmpty(parentCode) ? null : batchPortMap.get(parentCode);
|
||||||
|
if (Func.isNotEmpty(parent)) {
|
||||||
|
applyParent(portTerminal, parent);
|
||||||
|
// 父信息已由本批次港口回填,无需再回查数据库。
|
||||||
|
prepare(portTerminal, SOURCE_BATCH, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PortTerminal existingPortTerminal = baseMapper.selectByCodeIncludingDeleted(portTerminal.getCode());
|
portTerminal.setParentCode(parentCode);
|
||||||
if (existingPortTerminal == null) {
|
prepare(portTerminal, SOURCE_BATCH);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Objects.equals(existingPortTerminal.getIsDeleted(), 1)) {
|
|
||||||
throw new ServiceException("该编码已存在");
|
|
||||||
}
|
|
||||||
baseMapper.restoreById(existingPortTerminal.getId());
|
|
||||||
portTerminal.setId(existingPortTerminal.getId());
|
|
||||||
portTerminal.setIsDeleted(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatImportErrorMessage(List<String> validationErrors) {
|
/**
|
||||||
StringBuilder errorMessage = new StringBuilder();
|
* 将上级港口信息回填到码头,保证码头不会出现没有港口的数据。
|
||||||
for (int index = 0; index < validationErrors.size(); index++) {
|
*/
|
||||||
if (index > 0) {
|
private void applyParent(PortTerminal portTerminal, PortTerminal parent) {
|
||||||
errorMessage.append(System.lineSeparator());
|
portTerminal.setParentId(parent.getId());
|
||||||
|
portTerminal.setParentCode(parent.getCode());
|
||||||
|
portTerminal.setParentName(parent.getName());
|
||||||
|
// 与回查数据库保持一致:父港口区域信息为空时保留码头自身填写的值。
|
||||||
|
if (Func.isNotEmpty(parent.getCountry())) {
|
||||||
|
portTerminal.setCountry(parent.getCountry());
|
||||||
}
|
}
|
||||||
errorMessage.append(index + 1).append(". ").append(validationErrors.get(index));
|
if (Func.isNotEmpty(parent.getCity())) {
|
||||||
|
portTerminal.setCity(parent.getCity());
|
||||||
}
|
}
|
||||||
return errorMessage.toString();
|
if (Func.isNotEmpty(parent.getDistrictCode())) {
|
||||||
|
portTerminal.setDistrictCode(parent.getDistrictCode());
|
||||||
|
portTerminal.setRegionCode(parent.getDistrictCode());
|
||||||
|
} else if (Func.isNotEmpty(portTerminal.getDistrictCode())) {
|
||||||
|
portTerminal.setRegionCode(portTerminal.getDistrictCode());
|
||||||
}
|
}
|
||||||
|
if (Func.isNotEmpty(parent.getDistrictName())) {
|
||||||
private Map<String, Integer> buildImportCodeCountMap(List<PortTerminalExcel> data) {
|
portTerminal.setDistrictName(parent.getDistrictName());
|
||||||
Map<String, Integer> codeCountMap = new HashMap<>();
|
|
||||||
for (PortTerminalExcel excel : data) {
|
|
||||||
String code = resolveImportCode(excel);
|
|
||||||
if (Func.isNotEmpty(code)) {
|
|
||||||
codeCountMap.merge(code, 1, Integer::sum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return codeCountMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PortTerminal buildImportPortTerminal(PortTerminalExcel excel) {
|
|
||||||
PortTerminal portTerminal = new PortTerminal();
|
|
||||||
portTerminal.setCode(resolveImportCode(excel));
|
|
||||||
portTerminal.setName(excel.getName());
|
|
||||||
portTerminal.setCategory(excel.getCategory());
|
|
||||||
portTerminal.setParentName(excel.getParentName());
|
|
||||||
portTerminal.setParentCode(CATEGORY_TERMINAL.equals(trimToEmpty(excel.getCategory())) ? excel.getPortCode() : excel.getParentCode());
|
|
||||||
portTerminal.setCountry(excel.getCountry());
|
|
||||||
portTerminal.setProvinceName(excel.getProvinceName());
|
|
||||||
portTerminal.setCity(excel.getCity());
|
|
||||||
portTerminal.setDistrictName(excel.getDistrictName());
|
|
||||||
portTerminal.setDetailAddress(excel.getDetailAddress());
|
|
||||||
portTerminal.setLongitude(excel.getLongitude());
|
|
||||||
portTerminal.setLatitude(excel.getLatitude());
|
|
||||||
portTerminal.setRemark(excel.getRemark());
|
|
||||||
return portTerminal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveImportCode(PortTerminalExcel excel) {
|
|
||||||
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
|
|
||||||
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
|
|
||||||
if (Func.isEmpty(terminalCode)) {
|
|
||||||
return portCode;
|
|
||||||
}
|
|
||||||
return Func.isEmpty(portCode) ? terminalCode : portCode + "-" + terminalCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.setProvinceCode(trimToNull(portTerminal.getProvinceCode()));
|
|
||||||
portTerminal.setProvinceName(trimToNull(portTerminal.getProvinceName()));
|
|
||||||
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<String> validateImportPortTerminal(PortTerminalExcel excel, PortTerminal portTerminal,
|
|
||||||
Map<String, Integer> codeCountMap) {
|
|
||||||
List<String> validationErrors = new ArrayList<>();
|
|
||||||
if (!CATEGORY_PORT.equals(portTerminal.getCategory()) && !CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
|
|
||||||
addValidationError(validationErrors, "类型只能为港口或码头");
|
|
||||||
}
|
|
||||||
validateImportCodeColumns(excel, portTerminal, 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.<PortTerminal>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.getProvinceName(), 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);
|
|
||||||
if (Func.isEmpty(portTerminal.getDetailAddress())) {
|
|
||||||
addValidationError(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 validateImportCodeColumns(PortTerminalExcel excel, PortTerminal portTerminal,
|
|
||||||
List<String> validationErrors) {
|
|
||||||
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
|
|
||||||
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
|
|
||||||
String parentCode = trimToEmpty(excel.getParentCode()).toUpperCase(Locale.ROOT);
|
|
||||||
if (Func.isEmpty(portCode)) {
|
|
||||||
addValidationError(validationErrors, "港口编码不能为空");
|
|
||||||
}
|
|
||||||
if (!CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Func.isEmpty(terminalCode)) {
|
|
||||||
addValidationError(validationErrors, "码头编码不能为空");
|
|
||||||
}
|
|
||||||
if (Func.isEmpty(portTerminal.getParentName())) {
|
|
||||||
addValidationError(validationErrors, "上级港口不能为空");
|
|
||||||
}
|
|
||||||
if (Func.isEmpty(parentCode)) {
|
|
||||||
addValidationError(validationErrors, "上级港口编码不能为空");
|
|
||||||
} else if (Func.isNotEmpty(portCode) && !portCode.equals(parentCode) && !portCode.endsWith(parentCode)) {
|
|
||||||
addValidationError(validationErrors, "港口编码与上级港口编码不匹配");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateImportPortRegion(PortTerminal portTerminal, List<String> validationErrors) {
|
/**
|
||||||
if (Func.isEmpty(portTerminal.getCountry())) {
|
* 构造失败明细,行号按 Excel 中的实际行号(表头占第 1 行)推算。
|
||||||
addValidationError(validationErrors, "国家不能为空");
|
*/
|
||||||
|
private PortTerminalExcel buildFailure(List<PortTerminalExcel> data, int index, String message) {
|
||||||
|
PortTerminalExcel excel = data.get(index);
|
||||||
|
markFailure(data, index, message);
|
||||||
|
return excel;
|
||||||
}
|
}
|
||||||
if (Func.isEmpty(portTerminal.getCity())) {
|
|
||||||
addValidationError(validationErrors, "城市不能为空");
|
/**
|
||||||
}
|
* 仅标注错误原因,不重复加入失败明细集合。
|
||||||
if (Func.isEmpty(portTerminal.getProvinceName())) {
|
*/
|
||||||
addValidationError(validationErrors, "所属省份不能为空");
|
private void markFailure(List<PortTerminalExcel> data, int index, String message) {
|
||||||
}
|
if (index >= 0 && index < data.size()) {
|
||||||
if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isEmpty(portTerminal.getDistrictName())) {
|
data.get(index).setErrorMessage("第" + (index + 2) + "行:" + message);
|
||||||
addValidationError(validationErrors, "区县不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
fillRegion(portTerminal);
|
|
||||||
} catch (ServiceException exception) {
|
|
||||||
addValidationError(validationErrors, exception.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateImportParentPort(PortTerminal portTerminal, List<String> validationErrors) {
|
/**
|
||||||
if (Func.isEmpty(portTerminal.getParentCode())) {
|
* 解析异常信息,非业务异常统一提示导入失败。
|
||||||
addValidationError(validationErrors, "上级港口编码不能为空");
|
*/
|
||||||
return;
|
private String resolveMessage(Exception exception) {
|
||||||
}
|
return exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
||||||
PortTerminal parentPort = getOne(Wrappers.<PortTerminal>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<String> validationErrors) {
|
|
||||||
if (Func.isNotEmpty(value) && value.length() > maxLength) {
|
|
||||||
addValidationError(validationErrors, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addValidationError(List<String> validationErrors, String message) {
|
|
||||||
if (Func.isNotEmpty(message) && !validationErrors.contains(message)) {
|
|
||||||
validationErrors.add(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -430,12 +370,21 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void prepare(PortTerminal portTerminal, String defaultDataSource) {
|
private void prepare(PortTerminal portTerminal, String defaultDataSource) {
|
||||||
|
prepare(portTerminal, defaultDataSource, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 整理并补全港口码头数据。
|
||||||
|
*
|
||||||
|
* @param portTerminal 港口码头
|
||||||
|
* @param defaultDataSource 默认数据来源
|
||||||
|
* @param parentResolved 上级港口是否已确定(批量导入时由批次内存匹配得到,无需回查数据库)
|
||||||
|
*/
|
||||||
|
private void prepare(PortTerminal portTerminal, String defaultDataSource, boolean parentResolved) {
|
||||||
portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT));
|
portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT));
|
||||||
portTerminal.setCategory(trimToEmpty(portTerminal.getCategory()));
|
portTerminal.setCategory(trimToEmpty(portTerminal.getCategory()));
|
||||||
portTerminal.setName(trimToEmpty(portTerminal.getName()));
|
portTerminal.setName(trimToEmpty(portTerminal.getName()));
|
||||||
portTerminal.setCountry(trimToEmpty(portTerminal.getCountry()));
|
portTerminal.setCountry(trimToEmpty(portTerminal.getCountry()));
|
||||||
portTerminal.setProvinceCode(trimToNull(portTerminal.getProvinceCode()));
|
|
||||||
portTerminal.setProvinceName(trimToNull(portTerminal.getProvinceName()));
|
|
||||||
portTerminal.setCity(trimToEmpty(portTerminal.getCity()));
|
portTerminal.setCity(trimToEmpty(portTerminal.getCity()));
|
||||||
portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode()));
|
portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode()));
|
||||||
portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName()));
|
portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName()));
|
||||||
@@ -456,6 +405,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
portTerminal.setParentName(null);
|
portTerminal.setParentName(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (parentResolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
fillParentPort(portTerminal);
|
fillParentPort(portTerminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +423,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
.eq(PortTerminal::getIsDeleted, 0));
|
.eq(PortTerminal::getIsDeleted, 0));
|
||||||
}
|
}
|
||||||
if (Func.isEmpty(parent)) {
|
if (Func.isEmpty(parent)) {
|
||||||
throw new ServiceException("码头必须选择上级港口");
|
// 用专用异常类型:调用方需要区分"父港口真的漏填"与"父行自身失败",
|
||||||
|
// 后者不应连带给码头行报错(见 importTerminals)。
|
||||||
|
throw new ParentPortNotFoundException("码头必须选择上级港口");
|
||||||
}
|
}
|
||||||
if (!CATEGORY_PORT.equals(parent.getCategory())) {
|
if (!CATEGORY_PORT.equals(parent.getCategory())) {
|
||||||
throw new ServiceException("上级港口类型不正确");
|
throw new ServiceException("上级港口类型不正确");
|
||||||
@@ -479,31 +433,25 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
portTerminal.setParentId(parent.getId());
|
portTerminal.setParentId(parent.getId());
|
||||||
portTerminal.setParentCode(parent.getCode());
|
portTerminal.setParentCode(parent.getCode());
|
||||||
portTerminal.setParentName(parent.getName());
|
portTerminal.setParentName(parent.getName());
|
||||||
portTerminal.setCountry(inheritParentValue(parent.getCountry(), portTerminal.getCountry()));
|
// 父港口区域信息为空时保留码头自身填写的值,避免历史数据(区县为空)导致码头无法导入。
|
||||||
portTerminal.setProvinceCode(inheritParentValue(parent.getProvinceCode(), portTerminal.getProvinceCode()));
|
if (Func.isNotEmpty(parent.getCountry())) {
|
||||||
portTerminal.setProvinceName(inheritParentValue(parent.getProvinceName(), portTerminal.getProvinceName()));
|
portTerminal.setCountry(parent.getCountry());
|
||||||
portTerminal.setCity(inheritParentValue(parent.getCity(), portTerminal.getCity()));
|
}
|
||||||
portTerminal.setDistrictCode(inheritParentValue(parent.getDistrictCode(), portTerminal.getDistrictCode()));
|
if (Func.isNotEmpty(parent.getCity())) {
|
||||||
portTerminal.setDistrictName(inheritParentValue(parent.getDistrictName(), portTerminal.getDistrictName()));
|
portTerminal.setCity(parent.getCity());
|
||||||
portTerminal.setRegionCode(inheritParentValue(parent.getDistrictCode(), portTerminal.getRegionCode()));
|
}
|
||||||
if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isNotEmpty(portTerminal.getDistrictName())) {
|
if (Func.isNotEmpty(parent.getDistrictCode())) {
|
||||||
fillRegion(portTerminal);
|
portTerminal.setDistrictCode(parent.getDistrictCode());
|
||||||
|
portTerminal.setRegionCode(parent.getDistrictCode());
|
||||||
|
} else if (Func.isNotEmpty(portTerminal.getRegionCode())) {
|
||||||
|
portTerminal.setDistrictCode(portTerminal.getRegionCode());
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(parent.getDistrictName())) {
|
||||||
|
portTerminal.setDistrictName(parent.getDistrictName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillRegion(PortTerminal portTerminal) {
|
private void fillRegion(PortTerminal portTerminal) {
|
||||||
Region selectedProvince = null;
|
|
||||||
if (Func.isNotEmpty(portTerminal.getProvinceCode())) {
|
|
||||||
selectedProvince = regionService.getById(portTerminal.getProvinceCode());
|
|
||||||
}
|
|
||||||
if (Func.isEmpty(selectedProvince) && Func.isNotEmpty(portTerminal.getProvinceName())) {
|
|
||||||
selectedProvince = regionService.getOne(Wrappers.<Region>lambdaQuery()
|
|
||||||
.eq(Region::getName, portTerminal.getProvinceName())
|
|
||||||
.eq(Region::getRegionLevel, 1), false);
|
|
||||||
}
|
|
||||||
if (Func.isEmpty(selectedProvince)) {
|
|
||||||
throw new ServiceException("请选择所属省份");
|
|
||||||
}
|
|
||||||
Region district = null;
|
Region district = null;
|
||||||
if (Func.isNotEmpty(portTerminal.getDistrictCode())) {
|
if (Func.isNotEmpty(portTerminal.getDistrictCode())) {
|
||||||
district = regionService.getById(portTerminal.getDistrictCode());
|
district = regionService.getById(portTerminal.getDistrictCode());
|
||||||
@@ -512,7 +460,6 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
if (Func.isNotEmpty(portTerminal.getCity())) {
|
if (Func.isNotEmpty(portTerminal.getCity())) {
|
||||||
List<Region> cityList = regionService.list(Wrappers.<Region>lambdaQuery()
|
List<Region> cityList = regionService.list(Wrappers.<Region>lambdaQuery()
|
||||||
.eq(Region::getName, portTerminal.getCity())
|
.eq(Region::getName, portTerminal.getCity())
|
||||||
.eq(Region::getParentCode, selectedProvince.getCode())
|
|
||||||
.eq(Region::getRegionLevel, 2));
|
.eq(Region::getRegionLevel, 2));
|
||||||
for (Region city : cityList) {
|
for (Region city : cityList) {
|
||||||
district = regionService.getOne(Wrappers.<Region>lambdaQuery()
|
district = regionService.getOne(Wrappers.<Region>lambdaQuery()
|
||||||
@@ -536,26 +483,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
if (Func.isEmpty(city)) {
|
if (Func.isEmpty(city)) {
|
||||||
throw new ServiceException("区县所属城市不存在");
|
throw new ServiceException("区县所属城市不存在");
|
||||||
}
|
}
|
||||||
Region province = regionService.getById(city.getParentCode());
|
|
||||||
if (Func.isEmpty(province)) {
|
|
||||||
throw new ServiceException("城市所属省份不存在");
|
|
||||||
}
|
|
||||||
if (Func.isNotEmpty(portTerminal.getProvinceCode())
|
|
||||||
&& !Objects.equals(portTerminal.getProvinceCode(), province.getCode())) {
|
|
||||||
throw new ServiceException("城市与省份不匹配");
|
|
||||||
}
|
|
||||||
if (Func.isNotEmpty(portTerminal.getProvinceName())
|
|
||||||
&& !Objects.equals(portTerminal.getProvinceName(), province.getName())) {
|
|
||||||
throw new ServiceException("城市与省份不匹配");
|
|
||||||
}
|
|
||||||
if (!Objects.equals(selectedProvince.getCode(), province.getCode())) {
|
|
||||||
throw new ServiceException("城市与省份不匹配");
|
|
||||||
}
|
|
||||||
if (Func.isNotEmpty(portTerminal.getCity()) && !Objects.equals(portTerminal.getCity(), city.getName())) {
|
if (Func.isNotEmpty(portTerminal.getCity()) && !Objects.equals(portTerminal.getCity(), city.getName())) {
|
||||||
throw new ServiceException("区县与城市不匹配");
|
throw new ServiceException("区县与城市不匹配");
|
||||||
}
|
}
|
||||||
portTerminal.setProvinceCode(province.getCode());
|
|
||||||
portTerminal.setProvinceName(province.getName());
|
|
||||||
portTerminal.setCity(city.getName());
|
portTerminal.setCity(city.getName());
|
||||||
portTerminal.setDistrictCode(district.getCode());
|
portTerminal.setDistrictCode(district.getCode());
|
||||||
portTerminal.setDistrictName(district.getName());
|
portTerminal.setDistrictName(district.getName());
|
||||||
@@ -575,7 +505,6 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
validateLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字");
|
validateLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字");
|
||||||
validateLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字");
|
validateLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字");
|
||||||
validateLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字");
|
validateLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字");
|
||||||
validateLength(portTerminal.getProvinceName(), REGION_MAX_LENGTH, "所属省份不能超过50字");
|
|
||||||
validateLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字");
|
validateLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字");
|
||||||
validateLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字");
|
validateLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字");
|
||||||
validateLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字");
|
validateLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字");
|
||||||
@@ -596,15 +525,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
if (Func.isEmpty(portTerminal.getCity())) {
|
if (Func.isEmpty(portTerminal.getCity())) {
|
||||||
throw new ServiceException("城市不能为空");
|
throw new ServiceException("城市不能为空");
|
||||||
}
|
}
|
||||||
if (Func.isEmpty(portTerminal.getProvinceCode())) {
|
|
||||||
throw new ServiceException("请选择所属省份");
|
|
||||||
}
|
|
||||||
if (Func.isEmpty(portTerminal.getDistrictCode())) {
|
if (Func.isEmpty(portTerminal.getDistrictCode())) {
|
||||||
throw new ServiceException("区县不能为空");
|
throw new ServiceException("区县不能为空");
|
||||||
}
|
}
|
||||||
if (Func.isEmpty(portTerminal.getDetailAddress())) {
|
|
||||||
throw new ServiceException("详细地址不能为空");
|
|
||||||
}
|
|
||||||
validateDataSource(portTerminal.getDataSource());
|
validateDataSource(portTerminal.getDataSource());
|
||||||
validateStatus(portTerminal.getStatus());
|
validateStatus(portTerminal.getStatus());
|
||||||
validateRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE, "经度范围为 -180 到 180");
|
validateRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE, "经度范围为 -180 到 180");
|
||||||
@@ -655,12 +578,21 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
return Func.isEmpty(value) || (value.compareTo(min) >= 0 && value.compareTo(max) <= 0);
|
return Func.isEmpty(value) || (value.compareTo(min) >= 0 && value.compareTo(max) <= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归并导入行编码:仅填港口编码时取港口编码,填了码头编码时拼接为"港口编码-码头编码"。
|
||||||
|
*/
|
||||||
|
private String resolveImportCode(PortTerminalExcel excel) {
|
||||||
|
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
|
||||||
|
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
|
||||||
|
if (Func.isEmpty(terminalCode)) {
|
||||||
|
return portCode;
|
||||||
|
}
|
||||||
|
return Func.isEmpty(portCode) ? terminalCode : portCode + "-" + terminalCode;
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeDataSource(String dataSource) {
|
private String normalizeDataSource(String dataSource) {
|
||||||
String value = trimToEmpty(dataSource);
|
String value = trimToEmpty(dataSource);
|
||||||
if (SOURCE_INITIAL_IMPORT.equals(value) || SOURCE_INITIAL_OLD.equals(value)) {
|
return SOURCE_INITIAL_OLD.equals(value) ? SOURCE_INITIAL : value;
|
||||||
return SOURCE_INITIAL;
|
|
||||||
}
|
|
||||||
return SOURCE_MANUAL_OLD.equals(value) ? SOURCE_MANUAL : value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateEnabledTerminal(Long parentId) {
|
private void validateEnabledTerminal(Long parentId) {
|
||||||
@@ -686,9 +618,23 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
|||||||
return trimValue.isEmpty() ? null : trimValue;
|
return trimValue.isEmpty() ? null : trimValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String inheritParentValue(String parentValue, String currentValue) {
|
/**
|
||||||
String normalizedParentValue = trimToNull(parentValue);
|
* 上级港口找不到时抛出。
|
||||||
return normalizedParentValue == null ? currentValue : normalizedParentValue;
|
* <p>
|
||||||
|
* 与普通业务异常区分开,是因为调用方需要判断:这个父港口到底是"用户漏填了",
|
||||||
|
* 还是"父港口那一行就在本文件里、只是它自己校验失败"。后者不该连带给码头行报错。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
private static class ParentPortNotFoundException extends ServiceException {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
ParentPortNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-12
@@ -39,6 +39,7 @@ import org.springblade.common.constant.DataStatusEnum;
|
|||||||
import org.bouncycastle.util.encoders.Hex;
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
import org.springblade.common.constant.ParamConstant;
|
import org.springblade.common.constant.ParamConstant;
|
||||||
import org.springblade.common.constant.TenantConstant;
|
import org.springblade.common.constant.TenantConstant;
|
||||||
|
import org.springblade.common.utils.PasswordRuleUtil;
|
||||||
import org.springblade.core.cache.utils.CacheUtil;
|
import org.springblade.core.cache.utils.CacheUtil;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||||
@@ -76,7 +77,6 @@ import org.springblade.system.wrapper.UserWrapper;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
@@ -106,9 +106,6 @@ import static org.springblade.core.tenant.TenantGuard.EntityType.USER;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implements IUserService {
|
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implements IUserService {
|
||||||
private static final String GUEST_NAME = "guest";
|
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 String IAM_DEFAULT_DICT_CODE = "iam_default";
|
private static final String IAM_DEFAULT_DICT_CODE = "iam_default";
|
||||||
private static final String IAM_DEFAULT_ROLE_NAME = "默认角色";
|
private static final String IAM_DEFAULT_ROLE_NAME = "默认角色";
|
||||||
private static final String IAM_DEFAULT_DEPT_NAME = "默认部门";
|
private static final String IAM_DEFAULT_DEPT_NAME = "默认部门";
|
||||||
@@ -588,7 +585,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
List<User> userList = TenantGuard.verifyBatch(this, idList, USER);
|
List<User> userList = TenantGuard.verifyBatch(this, idList, USER);
|
||||||
Map<String, String> passwordMap = new LinkedHashMap<>();
|
Map<String, String> passwordMap = new LinkedHashMap<>();
|
||||||
for (User user : userList) {
|
for (User user : userList) {
|
||||||
String password = randomPassword();
|
String password = PasswordRuleUtil.generate();
|
||||||
User updateUser = new User();
|
User updateUser = new User();
|
||||||
updateUser.setPassword(DigestUtil.encrypt(password));
|
updateUser.setPassword(DigestUtil.encrypt(password));
|
||||||
updateUser.setUpdateTime(DateUtil.now());
|
updateUser.setUpdateTime(DateUtil.now());
|
||||||
@@ -604,6 +601,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
if (!Objects.equals(password, password2)) {
|
if (!Objects.equals(password, password2)) {
|
||||||
throw new ServiceException("两次输入密码不一致!");
|
throw new ServiceException("两次输入密码不一致!");
|
||||||
}
|
}
|
||||||
|
validatePlainPassword(password);
|
||||||
User user = TenantGuard.verify(this, userId, USER);
|
User user = TenantGuard.verify(this, userId, USER);
|
||||||
User updateUser = new User();
|
User updateUser = new User();
|
||||||
updateUser.setPassword(DigestUtil.encrypt(password));
|
updateUser.setPassword(DigestUtil.encrypt(password));
|
||||||
@@ -672,7 +670,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
if (user.getId() != null) {
|
if (user.getId() != null) {
|
||||||
this.updateUser(user);
|
this.updateUser(user);
|
||||||
} else {
|
} else {
|
||||||
user.setPassword(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
// 空密码交由 saveUser 按规则填充(初始密码不合规时自动生成)
|
||||||
|
user.setPassword(null);
|
||||||
this.submit(user);
|
this.submit(user);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -689,6 +688,9 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
*/
|
*/
|
||||||
private User buildImportUser(UserExcel userExcel, String tenantId) {
|
private User buildImportUser(UserExcel userExcel, String tenantId) {
|
||||||
User user = Objects.requireNonNull(BeanUtil.copyProperties(userExcel, User.class));
|
User user = Objects.requireNonNull(BeanUtil.copyProperties(userExcel, User.class));
|
||||||
|
// 宽容解析生日文本(2026-8-2 等写法),User.birthday 为 java.util.Date 需转换
|
||||||
|
java.time.LocalDate birthday = org.springblade.common.excel.LenientDateParser.parseDate(userExcel.getBirthday(), "生日");
|
||||||
|
user.setBirthday(birthday == null ? null : java.sql.Date.valueOf(birthday));
|
||||||
user.setTenantId(tenantId);
|
user.setTenantId(tenantId);
|
||||||
user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1));
|
user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1));
|
||||||
user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY));
|
user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY));
|
||||||
@@ -1061,8 +1063,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Func.isEmpty(user.getPassword())) {
|
if (Func.isEmpty(user.getPassword())) {
|
||||||
user.setPassword(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
String initPassword = ParamCache.getValue(DEFAULT_PARAM_PASSWORD);
|
||||||
|
user.setPassword(PasswordRuleUtil.isValid(initPassword) ? initPassword : PasswordRuleUtil.generate());
|
||||||
}
|
}
|
||||||
|
validatePlainPassword(user.getPassword());
|
||||||
user.setPassword(DigestUtil.encrypt(user.getPassword()));
|
user.setPassword(DigestUtil.encrypt(user.getPassword()));
|
||||||
Long userCount = baseMapper.selectCount(Wrappers.<User>query().lambda().eq(User::getTenantId, tenantId).eq(User::getAccount, user.getAccount()));
|
Long userCount = baseMapper.selectCount(Wrappers.<User>query().lambda().eq(User::getTenantId, tenantId).eq(User::getAccount, user.getAccount()));
|
||||||
if (userCount > 0L && Func.isEmpty(user.getId())) {
|
if (userCount > 0L && Func.isEmpty(user.getId())) {
|
||||||
@@ -1144,12 +1148,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private String randomPassword() {
|
private void validatePlainPassword(String password) {
|
||||||
StringBuilder password = new StringBuilder(RANDOM_PASSWORD_LENGTH);
|
if (!PasswordRuleUtil.isValid(password)) {
|
||||||
for (int index = 0; index < RANDOM_PASSWORD_LENGTH; index++) {
|
throw new ServiceException(PasswordRuleUtil.RULE_MESSAGE);
|
||||||
password.append(PASSWORD_CHARS.charAt(SECURE_RANDOM.nextInt(PASSWORD_CHARS.length())));
|
|
||||||
}
|
}
|
||||||
return password.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -6,14 +6,20 @@ package org.springblade.transport.config;
|
|||||||
|
|
||||||
import io.minio.MinioClient;
|
import io.minio.MinioClient;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 凭证图片 MinIO 客户端配置。
|
* 凭证图片 MinIO 客户端配置。
|
||||||
|
* <p>
|
||||||
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
|
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
|
||||||
|
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
|
||||||
|
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
|
||||||
|
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
|
||||||
public class VoucherMinioConfig {
|
public class VoucherMinioConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+5
-55
@@ -35,13 +35,11 @@ import org.springblade.core.mp.support.Query;
|
|||||||
import org.springblade.core.secure.annotation.PreAuth;
|
import org.springblade.core.secure.annotation.PreAuth;
|
||||||
import org.springblade.core.secure.constant.AuthConstant;
|
import org.springblade.core.secure.constant.AuthConstant;
|
||||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
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.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.CustomerArchiveVO;
|
||||||
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
|
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
|
||||||
import org.springblade.transport.service.ICustomerArchiveService;
|
import org.springblade.transport.service.ICustomerArchiveService;
|
||||||
|
import org.springblade.transport.service.impl.CustomerArchivePublicProcessService;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -49,8 +47,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +64,7 @@ import java.util.Map;
|
|||||||
public class CustomerArchivePublicController {
|
public class CustomerArchivePublicController {
|
||||||
|
|
||||||
private final ICustomerArchiveService customerArchiveService;
|
private final ICustomerArchiveService customerArchiveService;
|
||||||
private final IBusinessProcessClient businessProcessClient;
|
private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公开详情
|
* 公开详情
|
||||||
@@ -92,61 +88,15 @@ public class CustomerArchivePublicController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公开接收流程页 postMessage 数据(当前仅打印,便于联调)
|
* 公开接收流程页 postMessage 数据
|
||||||
*/
|
*/
|
||||||
@PostMapping("/process-message")
|
@PostMapping("/process-message")
|
||||||
@ApiOperationSupport(order = 3)
|
@ApiOperationSupport(order = 3)
|
||||||
@Operation(summary = "公开接收流程消息", description = "无需登录,接收后查询当前节点并打印")
|
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步客商")
|
||||||
public R processMessage(@RequestBody Map<String, Object> body) {
|
public R processMessage(@RequestBody Map<String, Object> body) {
|
||||||
log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body));
|
log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body));
|
||||||
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
|
customerArchivePublicProcessService.handleProcessMessage(body);
|
||||||
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");
|
return R.success("ok");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
FR<Object> 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<String, Object> asMap(Object value) {
|
|
||||||
if (!(value instanceof Map<?, ?> map)) {
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
|
||||||
map.forEach((key, nested) -> {
|
|
||||||
if (key != null) {
|
|
||||||
result.put(String.valueOf(key), nested);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String firstText(Map<String, Object> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package org.springblade.transport.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
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.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.mk.MkProcessMessageService;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MK 业务流程公开查看
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
@TenantIgnore
|
||||||
|
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||||
|
@RequestMapping("/mk-process/public")
|
||||||
|
@Tag(name = "MK业务流程公开查看", description = "MK业务流程公开查看")
|
||||||
|
public class MkProcessPublicController {
|
||||||
|
|
||||||
|
private final MkProcessMessageService mkProcessMessageService;
|
||||||
|
|
||||||
|
@GetMapping("/{bizType}/detail")
|
||||||
|
@ApiOperationSupport(order = 1)
|
||||||
|
@Operation(summary = "公开详情", description = "无需登录")
|
||||||
|
public R detail(@PathVariable String bizType,
|
||||||
|
@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||||
|
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{bizType}/process-message")
|
||||||
|
@ApiOperationSupport(order = 2)
|
||||||
|
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步业务")
|
||||||
|
public R processMessage(@PathVariable String bizType, @RequestBody Map<String, Object> body) {
|
||||||
|
log.info("公开页收到流程消息 bizType={} body={}", bizType, JSON.toJSONString(body));
|
||||||
|
mkProcessMessageService.handleProcessMessage(bizType, body);
|
||||||
|
return R.success("ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-5
@@ -52,6 +52,7 @@ import org.springblade.transport.pojo.entity.Waybill;
|
|||||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||||
import org.springblade.transport.pojo.vo.ProcessConfigVO;
|
import org.springblade.transport.pojo.vo.ProcessConfigVO;
|
||||||
import org.springblade.transport.service.IProcessConfigService;
|
import org.springblade.transport.service.IProcessConfigService;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -83,20 +84,34 @@ public class ProcessConfigController extends BladeController {
|
|||||||
private final VoucherFileMapper voucherFileMapper;
|
private final VoucherFileMapper voucherFileMapper;
|
||||||
private final VoucherImageMapper voucherImageMapper;
|
private final VoucherImageMapper voucherImageMapper;
|
||||||
private final VoucherManageMapper voucherManageMapper;
|
private final VoucherManageMapper voucherManageMapper;
|
||||||
private final MinioClient minioClient;
|
private final ObjectProvider<MinioClient> minioClientProvider;
|
||||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
|
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
|
||||||
private String minioBucketName;
|
private String minioBucketName;
|
||||||
|
|
||||||
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
|
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
|
||||||
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
|
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
|
||||||
VoucherManageMapper voucherManageMapper,
|
VoucherManageMapper voucherManageMapper,
|
||||||
MinioClient minioClient) {
|
ObjectProvider<MinioClient> minioClientProvider) {
|
||||||
this.processConfigService = processConfigService;
|
this.processConfigService = processConfigService;
|
||||||
this.waybillMapper = waybillMapper;
|
this.waybillMapper = waybillMapper;
|
||||||
this.voucherFileMapper = voucherFileMapper;
|
this.voucherFileMapper = voucherFileMapper;
|
||||||
this.voucherImageMapper = voucherImageMapper;
|
this.voucherImageMapper = voucherImageMapper;
|
||||||
this.voucherManageMapper = voucherManageMapper;
|
this.voucherManageMapper = voucherManageMapper;
|
||||||
this.minioClient = minioClient;
|
this.minioClientProvider = minioClientProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MinIO 客户端。
|
||||||
|
* <p>
|
||||||
|
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册,
|
||||||
|
* 此时凭证预览地址无法生成,抛出明确提示而非启动即失败。
|
||||||
|
*/
|
||||||
|
private MinioClient minioClient() {
|
||||||
|
MinioClient minioClient = minioClientProvider.getIfAvailable();
|
||||||
|
if (minioClient == null) {
|
||||||
|
throw new IllegalStateException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
|
||||||
|
}
|
||||||
|
return minioClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/detail")
|
@GetMapping("/detail")
|
||||||
@@ -147,7 +162,7 @@ public class ProcessConfigController extends BladeController {
|
|||||||
result.put("waybillNo", image.getWaybillNo());
|
result.put("waybillNo", image.getWaybillNo());
|
||||||
result.put("objectKey", image.getObjectKey());
|
result.put("objectKey", image.getObjectKey());
|
||||||
try {
|
try {
|
||||||
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||||
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
|
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
|
||||||
.expiry(1, TimeUnit.HOURS).build()));
|
.expiry(1, TimeUnit.HOURS).build()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -270,7 +285,7 @@ public class ProcessConfigController extends BladeController {
|
|||||||
result.put("objectKey", image.objectKey());
|
result.put("objectKey", image.objectKey());
|
||||||
result.put("matched", image.matched());
|
result.put("matched", image.matched());
|
||||||
try {
|
try {
|
||||||
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||||
.method(Method.GET).bucket(minioBucketName).object(image.objectKey())
|
.method(Method.GET).bucket(minioBucketName).object(image.objectKey())
|
||||||
.expiry(1, TimeUnit.HOURS).build()));
|
.expiry(1, TimeUnit.HOURS).build()));
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ public class ProjectApplyController extends BladeController {
|
|||||||
@ApiOperationSupport(order = 4)
|
@ApiOperationSupport(order = 4)
|
||||||
@Operation(summary = "新增或修改", description = "传入projectApply")
|
@Operation(summary = "新增或修改", description = "传入projectApply")
|
||||||
public R submit(@RequestBody ProjectApply projectApply) {
|
public R submit(@RequestBody ProjectApply projectApply) {
|
||||||
return R.status(projectApplyService.submit(projectApply));
|
return projectApplyService.submit(projectApply) ? R.data(projectApply) : R.fail("保存失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/submit-approval")
|
@PostMapping("/submit-approval")
|
||||||
|
|||||||
+1
-1
@@ -213,7 +213,7 @@ public class WaybillController extends BladeController {
|
|||||||
@ApiOperationSupport(order = 9)
|
@ApiOperationSupport(order = 9)
|
||||||
@Operation(summary = "新增或修改", description = "传入waybill")
|
@Operation(summary = "新增或修改", description = "传入waybill")
|
||||||
public R submit(@RequestBody Waybill waybill) {
|
public R submit(@RequestBody Waybill waybill) {
|
||||||
return R.status(waybillService.submit(waybill));
|
return waybillService.submit(waybill) ? R.data(waybill) : R.fail("保存失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/save-draft")
|
@PostMapping("/save-draft")
|
||||||
|
|||||||
+2
-3
@@ -36,7 +36,6 @@ import lombok.Data;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事故记录 Excel
|
* 事故记录 Excel
|
||||||
@@ -60,8 +59,8 @@ public class AccidentRecordExcel implements Serializable {
|
|||||||
@ExcelProperty("*车牌号/船号")
|
@ExcelProperty("*车牌号/船号")
|
||||||
private String vehicleNo;
|
private String vehicleNo;
|
||||||
|
|
||||||
@ExcelProperty("*事故发生日期")
|
@ExcelProperty(value = "*事故发生日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate accidentDate;
|
private String accidentDate;
|
||||||
|
|
||||||
@ExcelProperty("事故发生地点")
|
@ExcelProperty("事故发生地点")
|
||||||
private String accidentLocation;
|
private String accidentLocation;
|
||||||
|
|||||||
+3
-3
@@ -37,7 +37,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事故记录导出 Excel
|
* 事故记录导出 Excel
|
||||||
@@ -83,14 +83,14 @@ public class AccidentRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+4
-5
@@ -35,7 +35,6 @@ import lombok.Data;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 年检记录 Excel
|
* 年检记录 Excel
|
||||||
@@ -59,11 +58,11 @@ public class AnnualInspectionRecordExcel implements Serializable {
|
|||||||
@ExcelProperty("*车牌号/船号")
|
@ExcelProperty("*车牌号/船号")
|
||||||
private String vehicleNo;
|
private String vehicleNo;
|
||||||
|
|
||||||
@ExcelProperty("*检测评定日期")
|
@ExcelProperty(value = "*检测评定日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate inspectionAssessmentDate;
|
private String inspectionAssessmentDate;
|
||||||
|
|
||||||
@ExcelProperty("*有效期截止日")
|
@ExcelProperty(value = "*有效期截止日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate validUntilDate;
|
private String validUntilDate;
|
||||||
|
|
||||||
@ExcelProperty("*车辆技术等级")
|
@ExcelProperty("*车辆技术等级")
|
||||||
private String vehicleTechnicalLevel;
|
private String vehicleTechnicalLevel;
|
||||||
|
|||||||
+3
-3
@@ -36,7 +36,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 年检记录导出 Excel
|
* 年检记录导出 Excel
|
||||||
@@ -83,14 +83,14 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+3
@@ -24,6 +24,7 @@ package org.springblade.transport.excel;
|
|||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelIgnore;
|
import cn.idev.excel.annotation.ExcelIgnore;
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||||
@@ -88,9 +89,11 @@ public class CommonAddressExportExcel implements Serializable {
|
|||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -24,6 +24,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.format.NumberFormat;
|
import cn.idev.excel.annotation.format.NumberFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
@@ -93,9 +94,11 @@ public class CommonCargoExportExcel implements Serializable {
|
|||||||
private String deptName;
|
private String deptName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -23,6 +23,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||||
@@ -75,8 +76,10 @@ public class CommonRouteExportExcel implements Serializable {
|
|||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -23,6 +23,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.format.NumberFormat;
|
import cn.idev.excel.annotation.format.NumberFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
@@ -33,7 +34,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合同管理 Excel
|
* 合同管理 Excel
|
||||||
@@ -111,8 +112,10 @@ public class ContractManageExcel implements Serializable {
|
|||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
private LocalDateTime createTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
private LocalDateTime updateTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -7,12 +7,10 @@ package org.springblade.transport.excel;
|
|||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelIgnore;
|
import cn.idev.excel.annotation.ExcelIgnore;
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备台账 Excel
|
* 设备台账 Excel
|
||||||
@@ -29,7 +27,7 @@ public class EquipmentLedgerExcel implements Serializable {
|
|||||||
@ExcelProperty("设备品牌") private String equipmentBrand;
|
@ExcelProperty("设备品牌") private String equipmentBrand;
|
||||||
@ExcelProperty("设备类型") private String equipmentType;
|
@ExcelProperty("设备类型") private String equipmentType;
|
||||||
@ExcelProperty("规格型号") private String specificationModel;
|
@ExcelProperty("规格型号") private String specificationModel;
|
||||||
@ExcelProperty("出厂日期") @DateTimeFormat("yyyy-MM-dd") private LocalDate factoryDate;
|
@ExcelProperty(value = "出厂日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) private String factoryDate;
|
||||||
@ExcelProperty("备注") private String remark;
|
@ExcelProperty("备注") private String remark;
|
||||||
@ExcelIgnore private String errorMessage;
|
@ExcelIgnore private String errorMessage;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -11,6 +11,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ETC记录导出 Excel
|
* ETC记录导出 Excel
|
||||||
@@ -50,14 +51,14 @@ public class EtcRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+6
-7
@@ -35,7 +35,6 @@ import lombok.Data;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保险记录 Excel
|
* 保险记录 Excel
|
||||||
@@ -68,11 +67,11 @@ public class InsuranceRecordExcel implements Serializable {
|
|||||||
@ExcelProperty("*保单号")
|
@ExcelProperty("*保单号")
|
||||||
private String policyNo;
|
private String policyNo;
|
||||||
|
|
||||||
@ExcelProperty("*开始日期")
|
@ExcelProperty(value = "*开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate startDate;
|
private String startDate;
|
||||||
|
|
||||||
@ExcelProperty("*结束日期")
|
@ExcelProperty(value = "*结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate endDate;
|
private String endDate;
|
||||||
|
|
||||||
@ExcelProperty("保额")
|
@ExcelProperty("保额")
|
||||||
private BigDecimal insuredAmount;
|
private BigDecimal insuredAmount;
|
||||||
@@ -83,8 +82,8 @@ public class InsuranceRecordExcel implements Serializable {
|
|||||||
@ExcelProperty("发票号")
|
@ExcelProperty("发票号")
|
||||||
private String invoiceNo;
|
private String invoiceNo;
|
||||||
|
|
||||||
@ExcelProperty("开票日期")
|
@ExcelProperty(value = "开票日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate invoiceDate;
|
private String invoiceDate;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+3
-3
@@ -36,7 +36,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保险记录导出 Excel
|
* 保险记录导出 Excel
|
||||||
@@ -83,14 +83,14 @@ public class InsuranceRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+11
-15
@@ -33,22 +33,20 @@ import cn.idev.excel.metadata.data.WriteCellData;
|
|||||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||||
import cn.idev.excel.util.DateUtils;
|
import cn.idev.excel.util.DateUtils;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.format.DateTimeParseException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。
|
* 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。
|
||||||
|
* <p>
|
||||||
|
* 文本解析委托公共宽容解析器(支持 2026-8-2、2026/8/2 等写法,口径见根工作区
|
||||||
|
* docs/import-spec.md);无法识别的文本按既有行为抛出转换异常,
|
||||||
|
* 文案带原值,与「日期格式无法识别」口径一致。
|
||||||
*
|
*
|
||||||
* @author Chill
|
* @author Chill
|
||||||
*/
|
*/
|
||||||
public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> {
|
public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> {
|
||||||
|
|
||||||
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
||||||
private static final DateTimeFormatter DATE_TIME_MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<?> supportJavaTypeKey() {
|
public Class<?> supportJavaTypeKey() {
|
||||||
@@ -71,16 +69,14 @@ public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime
|
|||||||
if (value == null || value.trim().isEmpty()) {
|
if (value == null || value.trim().isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String normalizedValue = value.trim();
|
LocalDateTime parsed = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
|
||||||
try {
|
if (parsed != null) {
|
||||||
return LocalDateTime.parse(normalizedValue, DATE_TIME_FORMATTER);
|
return parsed;
|
||||||
} catch (DateTimeParseException ignored) {
|
|
||||||
try {
|
|
||||||
return LocalDateTime.parse(normalizedValue, DATE_TIME_MINUTE_FORMATTER);
|
|
||||||
} catch (DateTimeParseException ignoredMinute) {
|
|
||||||
return LocalDate.parse(normalizedValue, DATE_FORMATTER).atStartOfDay();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 无法识别的文本:与既有行为一致抛出转换异常(ExcelDataConvertException 是 RuntimeException,
|
||||||
|
// 会带上本文案一路抛到 Controller,由全局异常处理返回给前端提示)。
|
||||||
|
throw new cn.idev.excel.exception.ExcelDataConvertException(-1, -1, cellData, contentProperty,
|
||||||
|
value.trim() + " 日期格式无法识别");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+3
-3
@@ -11,7 +11,7 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 车辆保养记录导出 Excel
|
* 车辆保养记录导出 Excel
|
||||||
@@ -26,12 +26,12 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
|
|||||||
|
|
||||||
@ExcelProperty(value = "创建时间", index = 12)
|
@ExcelProperty(value = "创建时间", index = 12)
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty(value = "更新人", index = 13)
|
@ExcelProperty(value = "更新人", index = 13)
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty(value = "更新时间", index = 14)
|
@ExcelProperty(value = "更新时间", index = 14)
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -38,6 +38,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 车辆维修记录 Excel
|
* 车辆维修记录 Excel
|
||||||
@@ -95,16 +96,16 @@ public class MaintenanceRecordExcel implements Serializable {
|
|||||||
@NumberFormat("0.00")
|
@NumberFormat("0.00")
|
||||||
private BigDecimal mileage;
|
private BigDecimal mileage;
|
||||||
|
|
||||||
@ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class)
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class)
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("里程单位")
|
@ExcelProperty("里程单位")
|
||||||
private String mileageUnit;
|
private String mileageUnit;
|
||||||
|
|||||||
-4
@@ -25,7 +25,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@@ -42,7 +41,4 @@ public class MaintenanceRecordExportExcel extends MaintenanceRecordExcel {
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@ExcelProperty(value = "导出失败原因", index = 17)
|
|
||||||
private String errorMessage;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -36,7 +36,7 @@ import lombok.Data;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 里程记录导出 Excel
|
* 里程记录导出 Excel
|
||||||
@@ -72,14 +72,14 @@ public class MileageRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+3
-2
@@ -11,6 +11,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 油电记录导出 Excel
|
* 油电记录导出 Excel
|
||||||
@@ -61,14 +62,14 @@ public class OilElectricRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+2
-3
@@ -16,7 +16,6 @@ import lombok.Data;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 其他费用记录 Excel
|
* 其他费用记录 Excel
|
||||||
@@ -40,8 +39,8 @@ public class OtherExpenseRecordExcel implements Serializable {
|
|||||||
@ExcelProperty("*车牌号/船号")
|
@ExcelProperty("*车牌号/船号")
|
||||||
private String vehicleNo;
|
private String vehicleNo;
|
||||||
|
|
||||||
@ExcelProperty("*费用日期")
|
@ExcelProperty(value = "*费用日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate expenseDate;
|
private String expenseDate;
|
||||||
|
|
||||||
@ExcelProperty("*费用类型")
|
@ExcelProperty("*费用类型")
|
||||||
private String expenseType;
|
private String expenseType;
|
||||||
|
|||||||
+3
-3
@@ -11,7 +11,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 其他费用记录导出 Excel
|
* 其他费用记录导出 Excel
|
||||||
@@ -43,14 +43,14 @@ public class OtherExpenseRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+3
@@ -23,6 +23,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||||
@@ -69,8 +70,10 @@ public class ProcessConfigExportExcel implements Serializable {
|
|||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -23,6 +23,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||||
@@ -33,6 +34,7 @@ import java.io.Serializable;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目立项 Excel
|
* 项目立项 Excel
|
||||||
@@ -123,8 +125,10 @@ public class ProjectApplyExcel implements Serializable {
|
|||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
private LocalDateTime createTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
private LocalDateTime updateTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -23,6 +23,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||||
@@ -30,7 +31,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发货模板 Excel
|
* 发货模板 Excel
|
||||||
@@ -59,8 +60,10 @@ public class ShippingTemplateExcel implements Serializable {
|
|||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
private LocalDateTime updateTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date updateTime;
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
private LocalDateTime createTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -23,6 +23,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||||
@@ -32,7 +33,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 临时额度申请 Excel
|
* 临时额度申请 Excel
|
||||||
@@ -81,8 +82,10 @@ public class TemporaryCreditLimitExcel implements Serializable {
|
|||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
private LocalDateTime createTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
private LocalDateTime updateTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -38,6 +38,10 @@ import java.time.format.DateTimeFormatter;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。
|
* 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。
|
||||||
|
* <p>
|
||||||
|
* 数值日期序列号转 ISO 文本,文本原样透传;文本的宽容解析由服务层委托
|
||||||
|
* {@link org.springblade.common.excel.LenientDateParser} 完成(支持 2026-8-2 等写法,
|
||||||
|
* 口径见根工作区 docs/import-spec.md)。
|
||||||
*
|
*
|
||||||
* @author Chill
|
* @author Chill
|
||||||
*/
|
*/
|
||||||
|
|||||||
+3
-3
@@ -37,7 +37,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 换胎记录导出 Excel
|
* 换胎记录导出 Excel
|
||||||
@@ -76,14 +76,14 @@ public class TireReplacementRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+3
-3
@@ -28,7 +28,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 变更记录导出 Excel
|
* 变更记录导出 Excel
|
||||||
@@ -57,14 +57,14 @@ public class TransportChangeRecordExportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
+6
-3
@@ -23,6 +23,7 @@
|
|||||||
package org.springblade.transport.excel;
|
package org.springblade.transport.excel;
|
||||||
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||||
@@ -31,7 +32,7 @@ import lombok.Data;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运输计划 Excel
|
* 运输计划 Excel
|
||||||
@@ -92,8 +93,10 @@ public class TransportPlanExcel implements Serializable {
|
|||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
private LocalDateTime createTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
private LocalDateTime updateTime;
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -37,6 +37,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 违章记录 Excel
|
* 违章记录 Excel
|
||||||
@@ -95,14 +96,14 @@ public class ViolationRecordExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("创建时间")
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
|
|
||||||
@ExcelProperty("更新时间")
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelIgnore
|
@ExcelIgnore
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
|
|||||||
+5
-2
@@ -60,8 +60,11 @@ public class ViolationRecordImportExcel implements Serializable {
|
|||||||
@ExcelProperty("*驾驶人")
|
@ExcelProperty("*驾驶人")
|
||||||
private String driverName;
|
private String driverName;
|
||||||
|
|
||||||
@ExcelProperty("*类型/事项")
|
@ExcelProperty("*类型")
|
||||||
private String violationTypeOrItem;
|
private String violationType;
|
||||||
|
|
||||||
|
@ExcelProperty("*事项")
|
||||||
|
private String violationItem;
|
||||||
|
|
||||||
@ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class)
|
@ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
|||||||
+8
-8
@@ -108,10 +108,10 @@ public class WaybillExcel implements Serializable {
|
|||||||
private String escortPhone;
|
private String escortPhone;
|
||||||
@ExcelProperty("里程(km)")
|
@ExcelProperty("里程(km)")
|
||||||
private BigDecimal mileage;
|
private BigDecimal mileage;
|
||||||
@ExcelProperty("预计发货日期")
|
@ExcelProperty(value = "预计发货日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate estimatedStartTime;
|
private String estimatedStartTime;
|
||||||
@ExcelProperty("预计完成日期")
|
@ExcelProperty(value = "预计完成日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate estimatedEndTime;
|
private String estimatedEndTime;
|
||||||
@ExcelProperty("单价")
|
@ExcelProperty("单价")
|
||||||
private BigDecimal unitPrice;
|
private BigDecimal unitPrice;
|
||||||
@ExcelProperty("计价单位")
|
@ExcelProperty("计价单位")
|
||||||
@@ -126,10 +126,10 @@ public class WaybillExcel implements Serializable {
|
|||||||
private String businessStatus;
|
private String businessStatus;
|
||||||
@ExcelProperty("数据来源")
|
@ExcelProperty("数据来源")
|
||||||
private String dataSource;
|
private String dataSource;
|
||||||
@ExcelProperty("开始日期")
|
@ExcelProperty(value = "开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate startDate;
|
private String startDate;
|
||||||
@ExcelProperty("结束日期")
|
@ExcelProperty(value = "结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||||
private LocalDate endDate;
|
private String endDate;
|
||||||
@ExcelProperty("计划名称")
|
@ExcelProperty("计划名称")
|
||||||
private String planName;
|
private String planName;
|
||||||
@ExcelProperty("多联总单")
|
@ExcelProperty("多联总单")
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package org.springblade.transport.feign;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
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.transport.pojo.dto.CustomerProcessNodeSyncDTO;
|
||||||
|
import org.springblade.transport.service.ICustomerArchiveService;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客商档案 Feign实现
|
||||||
|
*/
|
||||||
|
@Hidden
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CustomerArchiveClient implements ICustomerArchiveClient {
|
||||||
|
|
||||||
|
private final ICustomerArchiveService customerArchiveService;
|
||||||
|
|
||||||
|
@TenantIgnore
|
||||||
|
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||||
|
@PostMapping(SYNC_PROCESS_NODE)
|
||||||
|
@Override
|
||||||
|
public FR<Boolean> syncProcessNode(@RequestBody CustomerProcessNodeSyncDTO param) {
|
||||||
|
if (param == null) {
|
||||||
|
return FR.data(false);
|
||||||
|
}
|
||||||
|
if (param.getProcessInfo() != null) {
|
||||||
|
return FR.data(customerArchiveService.syncProcessNodeFromProcessInfo(param.getId(), param.getProcessInfo()));
|
||||||
|
}
|
||||||
|
return FR.data(customerArchiveService.syncProcessNode(
|
||||||
|
param.getId(), param.getCurrentNode(), param.getCurrentProcessor(), param.getApprovalStatus()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package org.springblade.transport.feign;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
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.transport.mk.IMkProcessBizHandler;
|
||||||
|
import org.springblade.transport.mk.MkProcessMessageService;
|
||||||
|
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MK 业务流程同步 Feign 实现
|
||||||
|
*/
|
||||||
|
@Hidden
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MkProcessClient implements IMkProcessClient {
|
||||||
|
|
||||||
|
private final MkProcessMessageService mkProcessMessageService;
|
||||||
|
|
||||||
|
@TenantIgnore
|
||||||
|
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||||
|
@PostMapping(APPLY)
|
||||||
|
@Override
|
||||||
|
public FR<Boolean> apply(@RequestBody MkProcessSyncDTO param) {
|
||||||
|
if (param == null || param.getId() == null) {
|
||||||
|
return FR.data(false);
|
||||||
|
}
|
||||||
|
IMkProcessBizHandler handler = mkProcessMessageService.handler(param.getBizType());
|
||||||
|
String action = param.getAction() == null ? "sync" : param.getAction();
|
||||||
|
return switch (action) {
|
||||||
|
case "approve" -> FR.data(handler.approveFromProcess(param.getId(), param.getProcessorName()));
|
||||||
|
case "reject" -> FR.data(handler.rejectFromProcess(param.getId(), param.getProcessorName()));
|
||||||
|
default -> FR.data(handler.syncFromProcessInfo(param.getId(), param.getProcessInfo()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -22,9 +22,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.springblade.transport.mapper;
|
package org.springblade.transport.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
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.ProjectApply;
|
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目立项 Mapper 接口
|
* 项目立项 Mapper 接口
|
||||||
*
|
*
|
||||||
@@ -32,4 +38,26 @@ import org.springblade.transport.pojo.entity.ProjectApply;
|
|||||||
*/
|
*/
|
||||||
public interface ProjectApplyMapper extends BaseMapper<ProjectApply> {
|
public interface ProjectApplyMapper extends BaseMapper<ProjectApply> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按租户读取业务字典项,供未登录公开页回显下拉文案
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT dict_key AS dictKey, dict_value AS dictValue
|
||||||
|
FROM blade_dict_biz
|
||||||
|
WHERE is_deleted = 0 AND parent_id <> 0 AND tenant_id = #{tenantId} AND code = #{code}
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> selectDictBizItems(@Param("tenantId") String tenantId, @Param("code") String code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取系统字典项,供未登录公开页回显下拉文案
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT dict_key AS dictKey, dict_value AS dictValue
|
||||||
|
FROM blade_dict
|
||||||
|
WHERE is_deleted = 0 AND parent_id <> 0 AND code = #{code}
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> selectDictItems(@Param("code") String code);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package org.springblade.transport.mk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MK 业务流程处理器
|
||||||
|
*/
|
||||||
|
public interface IMkProcessBizHandler {
|
||||||
|
|
||||||
|
String bizType();
|
||||||
|
|
||||||
|
Object publicDetail(Long id);
|
||||||
|
|
||||||
|
boolean syncFromProcessInfo(Long id, Object processInfo);
|
||||||
|
|
||||||
|
boolean approveFromProcess(Long id, String processorName);
|
||||||
|
|
||||||
|
boolean rejectFromProcess(Long id, String processorName);
|
||||||
|
}
|
||||||
+439
@@ -0,0 +1,439 @@
|
|||||||
|
package org.springblade.transport.mk;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||||
|
import org.springblade.core.tool.utils.Func;
|
||||||
|
import org.springblade.transport.pojo.entity.ContractManage;
|
||||||
|
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||||
|
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||||
|
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||||
|
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||||
|
import org.springblade.transport.pojo.entity.Waybill;
|
||||||
|
import org.springblade.transport.service.IContractManageService;
|
||||||
|
import org.springblade.transport.service.ICustomerArchiveService;
|
||||||
|
import org.springblade.transport.service.IFormalSettlementService;
|
||||||
|
import org.springblade.transport.service.IPaymentApplicationService;
|
||||||
|
import org.springblade.transport.service.IPreSettlementService;
|
||||||
|
import org.springblade.transport.service.IProjectApplyService;
|
||||||
|
import org.springblade.transport.service.IWaybillService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 各业务 MK 流程处理器
|
||||||
|
*/
|
||||||
|
public final class MkProcessBizHandlers {
|
||||||
|
|
||||||
|
private MkProcessBizHandlers() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class CustomerHandler implements IMkProcessBizHandler {
|
||||||
|
private final ICustomerArchiveService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bizType() {
|
||||||
|
return "customer-archive";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object publicDetail(Long id) {
|
||||||
|
return service.publicDetail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
return service.syncProcessNodeFromProcessInfo(id, processInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
return service.approveFromProcess(id, processorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
return service.rejectFromProcess(id, processorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class ProjectHandler implements IMkProcessBizHandler {
|
||||||
|
private final IProjectApplyService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bizType() {
|
||||||
|
return "project-apply";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object publicDetail(Long id) {
|
||||||
|
return service.publicDetail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
ProjectApply entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||||
|
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||||
|
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(node)) {
|
||||||
|
entity.setCurrentNode(node);
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(processor)) {
|
||||||
|
entity.setCurrentProcessor(processor);
|
||||||
|
}
|
||||||
|
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
|
||||||
|
entity.setApprovalStatus("reviewing");
|
||||||
|
}
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
ProjectApply entity = service.getById(id);
|
||||||
|
if (entity == null || "approved".equals(entity.getApprovalStatus())
|
||||||
|
|| "change_approved".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||||
|
entity.setApprovalStatus(change ? "change_approved" : "approved");
|
||||||
|
entity.setEffectiveType("formal");
|
||||||
|
entity.setCurrentNode("审核通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
entity.setApprovedTime(LocalDateTime.now());
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
ProjectApply entity = service.getById(id);
|
||||||
|
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|
||||||
|
|| "change_rejected".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||||
|
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
|
||||||
|
entity.setCurrentNode("审核不通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class ContractHandler implements IMkProcessBizHandler {
|
||||||
|
private final IContractManageService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bizType() {
|
||||||
|
return "contract-manage";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object publicDetail(Long id) {
|
||||||
|
return service.publicDetail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
ContractManage entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||||
|
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||||
|
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(node)) {
|
||||||
|
entity.setCurrentNode(node);
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(processor)) {
|
||||||
|
entity.setCurrentProcessor(processor);
|
||||||
|
}
|
||||||
|
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
|
||||||
|
entity.setApprovalStatus("reviewing");
|
||||||
|
}
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
ContractManage entity = service.getById(id);
|
||||||
|
if (entity == null || "approved".equals(entity.getApprovalStatus())
|
||||||
|
|| "change_approved".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||||
|
entity.setApprovalStatus(change ? "change_approved" : "approved");
|
||||||
|
entity.setCurrentNode("审核通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
ContractManage entity = service.getById(id);
|
||||||
|
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|
||||||
|
|| "change_rejected".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||||
|
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
|
||||||
|
entity.setCurrentNode("审核不通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class WaybillHandler implements IMkProcessBizHandler {
|
||||||
|
private final IWaybillService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bizType() {
|
||||||
|
return "waybill-manage";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object publicDetail(Long id) {
|
||||||
|
return service.publicDetail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
Waybill entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||||
|
if (Func.isEmpty(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entity.setCurrentProcessNode(node);
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
Waybill entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entity.setCurrentProcessNode("审核通过");
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
Waybill entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entity.setCurrentProcessNode("审核不通过");
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class PreSettlementHandler implements IMkProcessBizHandler {
|
||||||
|
private final IPreSettlementService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bizType() {
|
||||||
|
return "pre-settlement";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object publicDetail(Long id) {
|
||||||
|
return service.detail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
PreSettlement entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||||
|
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||||
|
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(node)) {
|
||||||
|
entity.setCurrentNode(node);
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(processor)) {
|
||||||
|
entity.setCurrentProcessor(processor);
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("reviewing");
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
PreSettlement entity = service.getById(id);
|
||||||
|
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("approved");
|
||||||
|
entity.setCurrentNode("审核通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
entity.setApprovedTime(LocalDateTime.now());
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
PreSettlement entity = service.getById(id);
|
||||||
|
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("returned");
|
||||||
|
entity.setCurrentNode("审核不通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class FormalSettlementHandler implements IMkProcessBizHandler {
|
||||||
|
private final IFormalSettlementService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bizType() {
|
||||||
|
return "formal-settlement";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object publicDetail(Long id) {
|
||||||
|
return service.detail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
FormalSettlement entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||||
|
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||||
|
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(node)) {
|
||||||
|
entity.setCurrentNode(node);
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(processor)) {
|
||||||
|
entity.setCurrentProcessor(processor);
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("reviewing");
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
FormalSettlement entity = service.getById(id);
|
||||||
|
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("approved");
|
||||||
|
entity.setCurrentNode("审核通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
entity.setApprovedTime(LocalDateTime.now());
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
FormalSettlement entity = service.getById(id);
|
||||||
|
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("returned");
|
||||||
|
entity.setCurrentNode("审核不通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public static class PaymentHandler implements IMkProcessBizHandler {
|
||||||
|
private final IPaymentApplicationService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bizType() {
|
||||||
|
return "payment-application";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object publicDetail(Long id) {
|
||||||
|
return service.detail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
PaymentApplication entity = service.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||||
|
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||||
|
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(node)) {
|
||||||
|
entity.setCurrentNode(node);
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(processor)) {
|
||||||
|
entity.setCurrentProcessor(processor);
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("reviewing");
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
PaymentApplication entity = service.getById(id);
|
||||||
|
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("approved");
|
||||||
|
entity.setCurrentNode("审核通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
PaymentApplication entity = service.getById(id);
|
||||||
|
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
entity.setApprovalStatus("returned");
|
||||||
|
entity.setCurrentNode("审核不通过");
|
||||||
|
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||||
|
return service.updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
package org.springblade.transport.mk;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||||
|
import org.springblade.core.tool.api.FR;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springblade.process.feign.IBusinessProcessClient;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开页流程消息:当前节点立即查询,流程实例详情延迟 5 秒查询后同步业务状态。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MkProcessMessageService {
|
||||||
|
|
||||||
|
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
|
||||||
|
|
||||||
|
private final IBusinessProcessClient businessProcessClient;
|
||||||
|
private final List<IMkProcessBizHandler> handlers;
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "mk-process-info-" + THREAD_INDEX.incrementAndGet());
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
|
||||||
|
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IMkProcessBizHandler handler(String bizType) {
|
||||||
|
Map<String, IMkProcessBizHandler> mapping = handlers.stream()
|
||||||
|
.collect(Collectors.toMap(IMkProcessBizHandler::bizType, Function.identity(), (a, b) -> a));
|
||||||
|
IMkProcessBizHandler handler = mapping.get(bizType);
|
||||||
|
if (handler == null) {
|
||||||
|
throw new IllegalArgumentException("不支持的MK业务类型:" + bizType);
|
||||||
|
}
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleProcessMessage(String bizType, Map<String, Object> body) {
|
||||||
|
IMkProcessBizHandler handler = handler(bizType);
|
||||||
|
Map<String, Object> 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,跳过查询当前节点 bizType={}", bizType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String bizIdText = firstText(asMap(body == null ? null : body.get("formData")), "id", "formInstanceId");
|
||||||
|
queryCurrentNodes(bizType, processId, loginName);
|
||||||
|
scheduleProcessInfo(handler, bizType, processId, loginName, bizIdText, formValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queryCurrentNodes(String bizType, String processId, String loginName) {
|
||||||
|
try {
|
||||||
|
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
|
||||||
|
Object nodeData = result == null ? null : result.getData();
|
||||||
|
log.info("公开页流程消息当前节点详情 bizType={} processId={} loginName={} result={}",
|
||||||
|
bizType, processId, loginName, JSON.toJSONString(nodeData));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("公开页查询当前节点失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
|
||||||
|
String loginName, String bizIdText, Map<String, Object> formValues) {
|
||||||
|
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
|
||||||
|
log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={}", PROCESS_INFO_DELAY_SECONDS, bizType, processId);
|
||||||
|
scheduler.schedule(
|
||||||
|
() -> queryProcessInfo(handler, bizType, processId, loginName, bizIdText, formValuesCopy),
|
||||||
|
PROCESS_INFO_DELAY_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queryProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
|
||||||
|
String loginName, String bizIdText, Map<String, Object> formValues) {
|
||||||
|
try {
|
||||||
|
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
|
||||||
|
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
|
||||||
|
log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} result={}",
|
||||||
|
bizType, processId, loginName, JSON.toJSONString(processInfo));
|
||||||
|
if (isProcessFinished(processInfo)) {
|
||||||
|
applyProcessResult(handler, bizType, bizIdText, processId, formValues, true);
|
||||||
|
} else if (isProcessRejected(processInfo)) {
|
||||||
|
applyProcessResult(handler, bizType, bizIdText, processId, formValues, false);
|
||||||
|
} else if (StringUtil.isNotBlank(bizIdText)) {
|
||||||
|
try {
|
||||||
|
handler.syncFromProcessInfo(Long.valueOf(bizIdText), processInfo);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("公开页流程消息业务id格式不正确 bizType={} id={}", bizType, bizIdText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isProcessFinished(Object processInfo) {
|
||||||
|
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isProcessRejected(Object processInfo) {
|
||||||
|
Map<String, Object> info = asMap(processInfo);
|
||||||
|
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (hasItems(info.get("currentHandlers"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasItems(Object value) {
|
||||||
|
return value instanceof Collection<?> collection && !collection.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
|
||||||
|
String processId, Map<String, Object> formValues, boolean approved) {
|
||||||
|
String action = approved ? "审核通过" : "审核驳回";
|
||||||
|
if (StringUtil.isBlank(bizIdText)) {
|
||||||
|
log.warn("流程{}但未找到业务id,跳过同步 bizType={} processId={}", action, bizType, processId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
|
||||||
|
Long bizId = Long.valueOf(bizIdText);
|
||||||
|
if (approved) {
|
||||||
|
handler.approveFromProcess(bizId, processorName);
|
||||||
|
} else {
|
||||||
|
handler.rejectFromProcess(bizId, processorName);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> asMap(Object value) {
|
||||||
|
if (!(value instanceof Map<?, ?> map)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
map.forEach((key, nested) -> {
|
||||||
|
if (key != null) {
|
||||||
|
result.put(String.valueOf(key), nested);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstText(Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
package org.springblade.transport.mk;
|
||||||
|
|
||||||
|
import org.springblade.core.tool.jackson.JsonUtil;
|
||||||
|
import org.springblade.core.tool.utils.Func;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 MK 流程实例详情中的当前节点、当前处理人
|
||||||
|
*/
|
||||||
|
public final class MkProcessNodeHelper {
|
||||||
|
|
||||||
|
private MkProcessNodeHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String currentProcessor(Object processInfo) {
|
||||||
|
return joinDistinct(extractTexts(asMap(processInfo).get("currentHandlers"), "fdName", "name"), "、");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String currentNode(Object processInfo) {
|
||||||
|
Map<String, Object> taskInfo = asMap(asMap(processInfo).get("fdTaskInfo"));
|
||||||
|
return joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "、");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String text(String value, String fallback) {
|
||||||
|
return Func.isEmpty(value) ? fallback : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, Object> asMap(Object value) {
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
map.forEach((key, nested) -> {
|
||||||
|
if (key != null) {
|
||||||
|
result.put(String.valueOf(key), nested);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
|
||||||
|
return parsed == null ? Map.of() : parsed;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> extractTexts(Object listObj, String... keys) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (!(listObj instanceof Collection<?> collection) || keys == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (Object item : collection) {
|
||||||
|
Map<String, Object> map = asMap(item);
|
||||||
|
for (String key : keys) {
|
||||||
|
String text = Func.toStr(map.get(key), "").trim();
|
||||||
|
if (Func.isNotEmpty(text)) {
|
||||||
|
result.add(text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String joinDistinct(List<String> values, String delimiter) {
|
||||||
|
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -40,6 +40,14 @@ public interface IContractManageService extends BaseService<ContractManage> {
|
|||||||
|
|
||||||
IPage<ContractManageVO> selectContractManagePage(IPage<ContractManage> page, ContractManageVO contractManage);
|
IPage<ContractManageVO> selectContractManagePage(IPage<ContractManage> page, ContractManageVO contractManage);
|
||||||
ContractManageVO detail(Long id);
|
ContractManageVO detail(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开详情,不校验当前用户所属组织
|
||||||
|
*
|
||||||
|
* @param id 主键
|
||||||
|
* @return 合同详情
|
||||||
|
*/
|
||||||
|
ContractManageVO publicDetail(Long id);
|
||||||
boolean saveDraft(ContractManage contractManage);
|
boolean saveDraft(ContractManage contractManage);
|
||||||
boolean submit(ContractManage contractManage);
|
boolean submit(ContractManage contractManage);
|
||||||
boolean toTemporary(Long id);
|
boolean toTemporary(Long id);
|
||||||
|
|||||||
+47
@@ -100,6 +100,53 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
|
|||||||
*/
|
*/
|
||||||
boolean submitApproval(Long id);
|
boolean submitApproval(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 MK 当前节点同步客商当前节点、当前处理人和审批状态
|
||||||
|
*
|
||||||
|
* @param id 客商ID
|
||||||
|
* @param currentNode 当前节点
|
||||||
|
* @param currentProcessor 当前处理人
|
||||||
|
* @param approvalStatus 审批状态,可为空,为空时保持为审核中
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean syncProcessNode(Long id, String currentNode, String currentProcessor, String approvalStatus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 MK getCurrentNodes 返回结果同步客商当前节点
|
||||||
|
*
|
||||||
|
* @param id 客商ID
|
||||||
|
* @param currentNodes MK 当前节点详情
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean syncProcessNodeFromMk(Long id, Object currentNodes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 MK 流程实例详情回写当前节点、当前处理人
|
||||||
|
*
|
||||||
|
* @param id 客商ID
|
||||||
|
* @param processInfo MK getProcessInfo 返回数据
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean syncProcessNodeFromProcessInfo(Long id, Object processInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MK 流程结束(fdProcessStatus=30)时将客商置为审核通过
|
||||||
|
*
|
||||||
|
* @param id 客商ID
|
||||||
|
* @param processorName 当前处理人,可为空
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean approveFromProcess(Long id, String processorName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MK 流程状态为 20 时将客商置为审核驳回
|
||||||
|
*
|
||||||
|
* @param id 客商ID
|
||||||
|
* @param processorName 当前处理人,可为空
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean rejectFromProcess(Long id, String processorName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 撤回审批并恢复草稿状态
|
* 撤回审批并恢复草稿状态
|
||||||
*
|
*
|
||||||
|
|||||||
+8
@@ -40,6 +40,14 @@ public interface IProjectApplyService extends BaseService<ProjectApply> {
|
|||||||
|
|
||||||
IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply);
|
IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply);
|
||||||
ProjectApplyVO detail(Long id);
|
ProjectApplyVO detail(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开详情,不校验当前用户所属组织
|
||||||
|
*
|
||||||
|
* @param id 主键
|
||||||
|
* @return 项目立项详情
|
||||||
|
*/
|
||||||
|
ProjectApplyVO publicDetail(Long id);
|
||||||
Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply);
|
Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply);
|
||||||
Map<String, Object> changeRecordDetail(Long id, Integer recordIndex);
|
Map<String, Object> changeRecordDetail(Long id, Integer recordIndex);
|
||||||
boolean saveDraft(ProjectApply projectApply);
|
boolean saveDraft(ProjectApply projectApply);
|
||||||
|
|||||||
+7
@@ -37,6 +37,7 @@ import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO;
|
|||||||
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
|
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
|
||||||
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
|
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -82,6 +83,12 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
|||||||
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
|
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
|
||||||
void generateForCompletedWaybills(List<Long> waybillIds);
|
void generateForCompletedWaybills(List<Long> waybillIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运单维护里程后,同步未挂结算单的应收应付明细里程;
|
||||||
|
* 对按里程、按吨·公里计费的费用行按新里程重新计算。
|
||||||
|
*/
|
||||||
|
void syncMileageFromWaybill(Long waybillId, BigDecimal mileage);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
|
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
|
||||||
* <p>与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。</p>
|
* <p>与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。</p>
|
||||||
|
|||||||
+8
@@ -46,6 +46,14 @@ public interface IWaybillService extends BaseService<Waybill> {
|
|||||||
IPage<WaybillVO> selectWaybillPage(IPage<Waybill> page, WaybillVO waybill);
|
IPage<WaybillVO> selectWaybillPage(IPage<Waybill> page, WaybillVO waybill);
|
||||||
WaybillVO detail(Long id);
|
WaybillVO detail(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开详情,不校验当前用户所属组织
|
||||||
|
*
|
||||||
|
* @param id 主键
|
||||||
|
* @return 运单详情
|
||||||
|
*/
|
||||||
|
WaybillVO publicDetail(Long id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理端:运单打卡记录 + 司机上传凭证图(label=节点-凭证类型)
|
* 管理端:运单打卡记录 + 司机上传凭证图(label=节点-凭证类型)
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
@@ -94,6 +94,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
|
|||||||
AccidentRecordExcel excel = data.get(index);
|
AccidentRecordExcel excel = data.get(index);
|
||||||
try {
|
try {
|
||||||
AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class));
|
AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class));
|
||||||
|
accidentRecord.setAccidentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getAccidentDate(), "事故发生日期"));
|
||||||
prepare(accidentRecord);
|
prepare(accidentRecord);
|
||||||
List<String> validationErrors = validateImportAccidentRecord(accidentRecord);
|
List<String> validationErrors = validateImportAccidentRecord(accidentRecord);
|
||||||
if (Func.isNotEmpty(validationErrors)) {
|
if (Func.isNotEmpty(validationErrors)) {
|
||||||
|
|||||||
+2
@@ -113,6 +113,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
|
|||||||
AnnualInspectionRecordExcel excel = data.get(index);
|
AnnualInspectionRecordExcel excel = data.get(index);
|
||||||
try {
|
try {
|
||||||
AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class));
|
AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class));
|
||||||
|
annualInspectionRecord.setInspectionAssessmentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInspectionAssessmentDate(), "检测评定日期"));
|
||||||
|
annualInspectionRecord.setValidUntilDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getValidUntilDate(), "有效期截止日"));
|
||||||
prepare(annualInspectionRecord);
|
prepare(annualInspectionRecord);
|
||||||
List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord);
|
List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord);
|
||||||
if (Func.isNotEmpty(validationErrors)) {
|
if (Func.isNotEmpty(validationErrors)) {
|
||||||
|
|||||||
+14
@@ -28,6 +28,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||||
import org.springblade.core.secure.utils.AuthUtil;
|
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.jackson.JsonUtil;
|
||||||
import org.springblade.core.tool.utils.BeanUtil;
|
import org.springblade.core.tool.utils.BeanUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
@@ -97,6 +98,15 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
|||||||
return contractManageVO;
|
return contractManageVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TenantIgnore
|
||||||
|
public ContractManageVO publicDetail(Long id) {
|
||||||
|
ContractManageVO contractManageVO = ContractManageWrapper.build().entityVO(loadExists(id));
|
||||||
|
normalizeOptionalIntegerFields(contractManageVO);
|
||||||
|
contractManageVO.setReadonly(true);
|
||||||
|
return contractManageVO;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public boolean saveDraft(ContractManage contractManage) {
|
public boolean saveDraft(ContractManage contractManage) {
|
||||||
@@ -647,6 +657,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
|||||||
if (contractManage.getInvoiceCycle() != null && contractManage.getInvoiceCycle() <= 0) {
|
if (contractManage.getInvoiceCycle() != null && contractManage.getInvoiceCycle() <= 0) {
|
||||||
contractManage.setInvoiceCycle(null);
|
contractManage.setInvoiceCycle(null);
|
||||||
}
|
}
|
||||||
|
BigDecimal contractAmount = contractManage.getContractAmount();
|
||||||
|
if (contractAmount != null && contractAmount.compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||||
|
contractManage.setContractAmount(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateDraft(ContractManage contractManage) {
|
private void validateDraft(ContractManage contractManage) {
|
||||||
|
|||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||||
|
import org.springblade.core.tool.api.FR;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springblade.process.feign.IBusinessProcessClient;
|
||||||
|
import org.springblade.transport.service.ICustomerArchiveService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客商公开页流程消息处理:当前节点立即查询,流程实例详情延迟查询。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@TenantIgnore
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomerArchivePublicProcessService {
|
||||||
|
|
||||||
|
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
|
||||||
|
|
||||||
|
private final ICustomerArchiveService customerArchiveService;
|
||||||
|
private final IBusinessProcessClient businessProcessClient;
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "customer-archive-process-info-" + THREAD_INDEX.incrementAndGet());
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
|
||||||
|
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleProcessMessage(Map<String, Object> body) {
|
||||||
|
Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
String customerIdText = firstText(asMap(body == null ? null : body.get("formData")), "id");
|
||||||
|
queryCurrentNodes(processId, loginName);
|
||||||
|
scheduleProcessInfo(processId, loginName, customerIdText, formValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queryCurrentNodes(String processId, String loginName) {
|
||||||
|
try {
|
||||||
|
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
|
||||||
|
Object nodeData = result == null ? null : result.getData();
|
||||||
|
log.info("客商公开页流程消息当前节点详情 processId={} loginName={} result={}",
|
||||||
|
processId, loginName, JSON.toJSONString(nodeData));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("客商公开页查询当前节点失败 processId={} loginName={}", processId, loginName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleProcessInfo(String processId, String loginName, String customerIdText,
|
||||||
|
Map<String, Object> formValues) {
|
||||||
|
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
|
||||||
|
log.info("客商公开页将在{}秒后查询流程实例详情 processId={}", PROCESS_INFO_DELAY_SECONDS, processId);
|
||||||
|
scheduler.schedule(
|
||||||
|
() -> queryProcessInfo(processId, loginName, customerIdText, formValuesCopy),
|
||||||
|
PROCESS_INFO_DELAY_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queryProcessInfo(String processId, String loginName, String customerIdText,
|
||||||
|
Map<String, Object> formValues) {
|
||||||
|
try {
|
||||||
|
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
|
||||||
|
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
|
||||||
|
log.info("客商公开页流程消息流程实例详情 processId={} loginName={} result={}",
|
||||||
|
processId, loginName, JSON.toJSONString(processInfo));
|
||||||
|
if (isProcessFinished(processInfo)) {
|
||||||
|
applyProcessResult(customerIdText, processId, formValues, true);
|
||||||
|
} else if (isProcessRejected(processInfo)) {
|
||||||
|
applyProcessResult(customerIdText, processId, formValues, false);
|
||||||
|
} else if (StringUtil.isNotBlank(customerIdText)) {
|
||||||
|
try {
|
||||||
|
customerArchiveService.syncProcessNodeFromProcessInfo(Long.valueOf(customerIdText), processInfo);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("客商公开页流程消息客商id格式不正确:{}", customerIdText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("客商公开页查询流程实例详情失败 processId={} loginName={}", processId, loginName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isProcessFinished(Object processInfo) {
|
||||||
|
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isProcessRejected(Object processInfo) {
|
||||||
|
Map<String, Object> info = asMap(processInfo);
|
||||||
|
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (hasItems(info.get("currentHandlers"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasItems(Object value) {
|
||||||
|
return value instanceof Collection<?> collection && !collection.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProcessResult(String customerIdText, String processId, Map<String, Object> formValues,
|
||||||
|
boolean approved) {
|
||||||
|
String action = approved ? "审核通过" : "审核驳回";
|
||||||
|
if (StringUtil.isBlank(customerIdText)) {
|
||||||
|
log.warn("流程{}但未找到客商id,跳过同步 processId={}", action, processId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
|
||||||
|
Long customerId = Long.valueOf(customerIdText);
|
||||||
|
if (approved) {
|
||||||
|
customerArchiveService.approveFromProcess(customerId, processorName);
|
||||||
|
} else {
|
||||||
|
customerArchiveService.rejectFromProcess(customerId, processorName);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("流程{}但客商id格式不正确:{}", action, customerIdText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> asMap(Object value) {
|
||||||
|
if (!(value instanceof Map<?, ?> map)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
map.forEach((key, nested) -> {
|
||||||
|
if (key != null) {
|
||||||
|
result.put(String.valueOf(key), nested);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstText(Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+226
@@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||||
import org.springblade.core.secure.utils.AuthUtil;
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
@@ -85,6 +86,7 @@ import java.math.RoundingMode;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -101,6 +103,7 @@ import java.util.stream.Collectors;
|
|||||||
*
|
*
|
||||||
* @author Chill
|
* @author Chill
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveMapper, CustomerArchive> implements ICustomerArchiveService {
|
public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveMapper, CustomerArchive> implements ICustomerArchiveService {
|
||||||
@@ -214,12 +217,151 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class));
|
CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class));
|
||||||
CustomerArchive after = copyCustomer(before);
|
CustomerArchive after = copyCustomer(before);
|
||||||
after.setApprovalStatus(APPROVAL_REVIEWING);
|
after.setApprovalStatus(APPROVAL_REVIEWING);
|
||||||
|
if (Func.isEmpty(after.getCurrentNode()) || Objects.equals(after.getCurrentNode(), "草稿")) {
|
||||||
after.setCurrentNode("客商准入审批");
|
after.setCurrentNode("客商准入审批");
|
||||||
after.setCurrentProcessor("待处理");
|
after.setCurrentProcessor("待处理");
|
||||||
|
}
|
||||||
addChangeRecord(id, "提交客商准入审批", before, after);
|
addChangeRecord(id, "提交客商准入审批", before, after);
|
||||||
return updateById(after);
|
return updateById(after);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TenantIgnore
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean syncProcessNode(Long id, String currentNode, String currentProcessor, String approvalStatus) {
|
||||||
|
if (Func.isEmpty(id)) {
|
||||||
|
log.warn("同步客商当前节点失败,客商ID为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CustomerArchive customer = getById(id);
|
||||||
|
if (customer == null || Objects.equals(customer.getIsDeleted(), 1)) {
|
||||||
|
log.warn("同步客商当前节点失败,客商不存在 id={}", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String nextStatus = Func.isEmpty(approvalStatus) ? APPROVAL_REVIEWING : approvalStatus;
|
||||||
|
boolean updated = this.lambdaUpdate()
|
||||||
|
.eq(CustomerArchive::getId, id)
|
||||||
|
.set(Func.isNotEmpty(currentNode), CustomerArchive::getCurrentNode, currentNode)
|
||||||
|
.set(Func.isNotEmpty(currentProcessor), CustomerArchive::getCurrentProcessor, currentProcessor)
|
||||||
|
.set(Func.isNotEmpty(nextStatus), CustomerArchive::getApprovalStatus, nextStatus)
|
||||||
|
.update();
|
||||||
|
log.info("同步客商当前节点 id={} currentNode={} currentProcessor={} approvalStatus={} result={}",
|
||||||
|
id, currentNode, currentProcessor, nextStatus, updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TenantIgnore
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean syncProcessNodeFromMk(Long id, Object currentNodes) {
|
||||||
|
if (Func.isEmpty(id) || currentNodes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> nodes = normalizeMkNodeList(currentNodes);
|
||||||
|
if (nodes.isEmpty()) {
|
||||||
|
log.warn("同步客商当前节点跳过,节点详情为空 id={}", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<String> nodeNames = new ArrayList<>();
|
||||||
|
List<String> handlerNames = new ArrayList<>();
|
||||||
|
for (Map<String, Object> node : nodes) {
|
||||||
|
String nodeName = Func.toStr(node.get("nodeName"), "").trim();
|
||||||
|
if (Func.isNotEmpty(nodeName)) {
|
||||||
|
nodeNames.add(nodeName);
|
||||||
|
}
|
||||||
|
Object handlers = node.get("nodeHandlers");
|
||||||
|
if (!(handlers instanceof Collection<?> handlerList)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Object handler : handlerList) {
|
||||||
|
Map<String, Object> handlerMap = asStringObjectMap(handler);
|
||||||
|
String handlerName = Func.toStr(handlerMap.get("handlerName"), "").trim();
|
||||||
|
if (Func.isNotEmpty(handlerName)) {
|
||||||
|
handlerNames.add(handlerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return syncProcessNode(id, joinDistinct(nodeNames), joinDistinct(handlerNames), APPROVAL_REVIEWING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TenantIgnore
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean syncProcessNodeFromProcessInfo(Long id, Object processInfo) {
|
||||||
|
if (Func.isEmpty(id) || processInfo == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Map<String, Object> info = asStringObjectMap(processInfo);
|
||||||
|
if (info.isEmpty()) {
|
||||||
|
log.warn("同步客商当前节点跳过,流程实例详情为空 id={}", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String currentProcessor = joinDistinct(extractTexts(info.get("currentHandlers"), "fdName", "name"), "、");
|
||||||
|
Map<String, Object> taskInfo = asStringObjectMap(info.get("fdTaskInfo"));
|
||||||
|
String currentNode = joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "、");
|
||||||
|
if (Func.isEmpty(currentNode) && Func.isEmpty(currentProcessor)) {
|
||||||
|
log.warn("同步客商当前节点跳过,未解析到节点名称或处理人 id={}", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return syncProcessNode(id, currentNode, currentProcessor, APPROVAL_REVIEWING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TenantIgnore
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean approveFromProcess(Long id, String processorName) {
|
||||||
|
if (Func.isEmpty(id)) {
|
||||||
|
log.warn("流程结束审核通过失败,客商ID为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CustomerArchive before = getById(id);
|
||||||
|
if (before == null || Objects.equals(before.getIsDeleted(), 1)) {
|
||||||
|
log.warn("流程结束审核通过失败,客商不存在 id={}", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Objects.equals(before.getApprovalStatus(), APPROVAL_APPROVED)) {
|
||||||
|
log.info("流程结束审核通过跳过,客商已是审核通过 id={}", id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
CustomerArchive after = copyCustomer(before);
|
||||||
|
after.setAccessType(ACCESS_FORMAL);
|
||||||
|
after.setApprovalStatus(APPROVAL_APPROVED);
|
||||||
|
after.setCurrentNode("审核通过");
|
||||||
|
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
|
||||||
|
after.setApprovedTime(LocalDateTime.now());
|
||||||
|
addChangeRecord(id, "客商准入审核通过", before, after);
|
||||||
|
boolean updated = updateById(after);
|
||||||
|
log.info("流程结束审核通过 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TenantIgnore
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean rejectFromProcess(Long id, String processorName) {
|
||||||
|
if (Func.isEmpty(id)) {
|
||||||
|
log.warn("流程审核驳回失败,客商ID为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CustomerArchive before = getById(id);
|
||||||
|
if (before == null || Objects.equals(before.getIsDeleted(), 1)) {
|
||||||
|
log.warn("流程审核驳回失败,客商不存在 id={}", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Objects.equals(before.getApprovalStatus(), APPROVAL_REJECTED)) {
|
||||||
|
log.info("流程审核驳回跳过,客商已是审核不通过 id={}", id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
CustomerArchive after = copyCustomer(before);
|
||||||
|
after.setApprovalStatus(APPROVAL_REJECTED);
|
||||||
|
after.setCurrentNode("审核不通过");
|
||||||
|
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
|
||||||
|
addChangeRecord(id, "客商准入审核不通过", before, after);
|
||||||
|
boolean updated = updateById(after);
|
||||||
|
log.info("流程审核驳回 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public boolean withdrawApproval(Long id) {
|
public boolean withdrawApproval(Long id) {
|
||||||
@@ -479,6 +621,23 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
if (Func.isEmpty(accountVO.getAccountName()) && Func.isEmpty(accountVO.getBankAccount())) {
|
if (Func.isEmpty(accountVO.getAccountName()) && Func.isEmpty(accountVO.getBankAccount())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (Func.isEmpty(accountVO.getAccountName())) {
|
||||||
|
throw new ServiceException("收款单位名称不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(accountVO.getAccountHolderName())) {
|
||||||
|
throw new ServiceException("开户人姓名不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(accountVO.getBankName())) {
|
||||||
|
throw new ServiceException("开户行不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(accountVO.getCnapsCode())) {
|
||||||
|
throw new ServiceException("联行号不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(accountVO.getBankAccount())) {
|
||||||
|
throw new ServiceException("收款账号不能为空");
|
||||||
|
}
|
||||||
|
validateLength(accountVO.getCnapsCode(), 32, "联行号最多32个字符");
|
||||||
|
validateLength(accountVO.getRemark(), 200, "收款信息备注最多200个字符");
|
||||||
CustomerReceiptAccount account = Objects.requireNonNull(BeanUtil.copyProperties(accountVO, CustomerReceiptAccount.class));
|
CustomerReceiptAccount account = Objects.requireNonNull(BeanUtil.copyProperties(accountVO, CustomerReceiptAccount.class));
|
||||||
account.setId(IdWorker.getId());
|
account.setId(IdWorker.getId());
|
||||||
account.setCustomerId(customerId);
|
account.setCustomerId(customerId);
|
||||||
@@ -1032,6 +1191,73 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
return Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
|
return Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Map<String, Object>> normalizeMkNodeList(Object currentNodes) {
|
||||||
|
Object source = currentNodes;
|
||||||
|
if (source instanceof String text && Func.isNotEmpty(text.trim())) {
|
||||||
|
source = JsonUtil.parse(text, List.class);
|
||||||
|
}
|
||||||
|
if (source instanceof Collection<?> collection) {
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Object item : collection) {
|
||||||
|
Map<String, Object> map = asStringObjectMap(item);
|
||||||
|
if (!map.isEmpty()) {
|
||||||
|
result.add(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Map<String, Object> single = asStringObjectMap(source);
|
||||||
|
return single.isEmpty() ? List.of() : List.of(single);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> asStringObjectMap(Object value) {
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
map.forEach((key, nested) -> {
|
||||||
|
if (key != null) {
|
||||||
|
result.put(String.valueOf(key), nested);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
|
||||||
|
return parsed == null ? Map.of() : parsed;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinDistinct(List<String> values) {
|
||||||
|
return joinDistinct(values, ",");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinDistinct(List<String> values, String delimiter) {
|
||||||
|
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> extractTexts(Object listObj, String... keys) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (!(listObj instanceof Collection<?> collection) || keys == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (Object item : collection) {
|
||||||
|
Map<String, Object> map = asStringObjectMap(item);
|
||||||
|
for (String key : keys) {
|
||||||
|
String text = Func.toStr(map.get(key), "").trim();
|
||||||
|
if (Func.isNotEmpty(text)) {
|
||||||
|
result.add(text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) {
|
private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) {
|
||||||
Map<String, Object> beforeSnapshot = customerSnapshot(before);
|
Map<String, Object> beforeSnapshot = customerSnapshot(before);
|
||||||
Map<String, Object> afterSnapshot = customerSnapshot(after);
|
Map<String, Object> afterSnapshot = customerSnapshot(after);
|
||||||
|
|||||||
+1
@@ -91,6 +91,7 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
|
|||||||
EquipmentLedgerExcel excel = data.get(index);
|
EquipmentLedgerExcel excel = data.get(index);
|
||||||
try {
|
try {
|
||||||
EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class));
|
EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class));
|
||||||
|
equipmentLedger.setFactoryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getFactoryDate(), "出厂日期"));
|
||||||
prepare(equipmentLedger);
|
prepare(equipmentLedger);
|
||||||
if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) {
|
if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) {
|
||||||
equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes));
|
equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes));
|
||||||
|
|||||||
+3
@@ -119,6 +119,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
|
|||||||
InsuranceRecordExcel excel = data.get(index);
|
InsuranceRecordExcel excel = data.get(index);
|
||||||
try {
|
try {
|
||||||
InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class));
|
InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class));
|
||||||
|
insuranceRecord.setStartDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getStartDate(), "开始日期"));
|
||||||
|
insuranceRecord.setEndDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEndDate(), "结束日期"));
|
||||||
|
insuranceRecord.setInvoiceDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInvoiceDate(), "开票日期"));
|
||||||
prepare(insuranceRecord);
|
prepare(insuranceRecord);
|
||||||
List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord);
|
List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord);
|
||||||
if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) {
|
if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) {
|
||||||
|
|||||||
+36
-1
@@ -174,6 +174,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
|||||||
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
|
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
|
||||||
}
|
}
|
||||||
Waybill first = waybills.get(0);
|
Waybill first = waybills.get(0);
|
||||||
|
Waybill last = waybills.get(waybills.size() - 1);
|
||||||
LoadingManage loadingManage = new LoadingManage();
|
LoadingManage loadingManage = new LoadingManage();
|
||||||
loadingManage.setLoadingNo(normalizedLoadingNo);
|
loadingManage.setLoadingNo(normalizedLoadingNo);
|
||||||
loadingManage.setLoadingSubNos(waybills.stream()
|
loadingManage.setLoadingSubNos(waybills.stream()
|
||||||
@@ -197,7 +198,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
|||||||
loadingManage.setCarrierName(first.getCarrierName());
|
loadingManage.setCarrierName(first.getCarrierName());
|
||||||
loadingManage.setCarrierContractId(first.getCarrierContractId());
|
loadingManage.setCarrierContractId(first.getCarrierContractId());
|
||||||
loadingManage.setDepartureAddress(first.getDepartureAddress());
|
loadingManage.setDepartureAddress(first.getDepartureAddress());
|
||||||
loadingManage.setArrivalAddress(first.getArrivalAddress());
|
loadingManage.setTransitAddress(buildImportedRouteTransitAddress(waybills));
|
||||||
|
loadingManage.setArrivalAddress(last.getArrivalAddress());
|
||||||
loadingManage.setOriginalNo(first.getOriginalNo());
|
loadingManage.setOriginalNo(first.getOriginalNo());
|
||||||
loadingManage.setDataSource("批量导入");
|
loadingManage.setDataSource("批量导入");
|
||||||
loadingManage.setStartDate(first.getStartDate());
|
loadingManage.setStartDate(first.getStartDate());
|
||||||
@@ -222,6 +224,39 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
|||||||
.set(Waybill::getLoadingNo, normalizedLoadingNo));
|
.set(Waybill::getLoadingNo, normalizedLoadingNo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据导入运单构建配载单的途经地:
|
||||||
|
* 组内运单按导入顺序串联路线,上一票的到货地址与下一票的发货地址相同(中途卸货点)时只保留一个点,
|
||||||
|
* 最终形成“首票发货地 → 途经点 → 末票到货地”。
|
||||||
|
*/
|
||||||
|
private String buildImportedRouteTransitAddress(List<Waybill> waybills) {
|
||||||
|
// 按顺序收集全部节点:首票发货地、每票到货地;节点与相邻前一点相同则重合跳过
|
||||||
|
List<String> nodes = new ArrayList<>();
|
||||||
|
for (Waybill waybill : waybills) {
|
||||||
|
String departure = TransportBusinessSupport.trimToNull(waybill.getDepartureAddress());
|
||||||
|
String arrival = TransportBusinessSupport.trimToNull(waybill.getArrivalAddress());
|
||||||
|
appendRouteNode(nodes, departure);
|
||||||
|
appendRouteNode(nodes, arrival);
|
||||||
|
}
|
||||||
|
// 途经点 = 去掉首尾(首票发货地、末票到货地)后的中间节点
|
||||||
|
List<String> transitNodes = nodes.size() > 2 ? nodes.subList(1, nodes.size() - 1) : List.of();
|
||||||
|
if (Func.isEmpty(transitNodes)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return String.join(" - ", transitNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRouteNode(List<String> nodes, String address) {
|
||||||
|
if (Func.isEmpty(address)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!nodes.isEmpty() && nodes.get(nodes.size() - 1).equals(address)) {
|
||||||
|
// 与上一节点相同视为同一地点,重合不重复
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nodes.add(address);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public BusinessRemoveResultVO removeLoadingManage(String ids) {
|
public BusinessRemoveResultVO removeLoadingManage(String ids) {
|
||||||
|
|||||||
+1
@@ -75,6 +75,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
|
|||||||
OtherExpenseRecordExcel excel = data.get(index);
|
OtherExpenseRecordExcel excel = data.get(index);
|
||||||
try {
|
try {
|
||||||
OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class));
|
OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class));
|
||||||
|
otherExpenseRecord.setExpenseDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpenseDate(), "费用日期"));
|
||||||
otherExpenseRecord.setDataSource("批量导入");
|
otherExpenseRecord.setDataSource("批量导入");
|
||||||
prepare(otherExpenseRecord);
|
prepare(otherExpenseRecord);
|
||||||
List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord);
|
List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord);
|
||||||
|
|||||||
+1
-1
@@ -598,7 +598,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
|||||||
if (Func.isEmpty(request.getRows())) {
|
if (Func.isEmpty(request.getRows())) {
|
||||||
throw new ServiceException("请填写需要调整的费用行");
|
throw new ServiceException("请填写需要调整的费用行");
|
||||||
}
|
}
|
||||||
String changeReason = requiredText(limitRemark(request.getChangeReason(), 200), "调整原因");
|
String changeReason = limitRemark(request.getChangeReason(), 200);
|
||||||
Map<Long, PreSettlementDetailFee> existingMap = detailFees(detail.getId()).stream()
|
Map<Long, PreSettlementDetailFee> existingMap = detailFees(detail.getId()).stream()
|
||||||
.collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity()));
|
.collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity()));
|
||||||
if (request.getRows().size() != existingMap.size()) {
|
if (request.getRows().size() != existingMap.size()) {
|
||||||
|
|||||||
+69
@@ -26,9 +26,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||||
import org.springblade.core.secure.utils.AuthUtil;
|
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.jackson.JsonUtil;
|
||||||
import org.springblade.core.tool.utils.BeanUtil;
|
import org.springblade.core.tool.utils.BeanUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
@@ -70,6 +72,7 @@ import java.util.stream.Collectors;
|
|||||||
* @author Chill
|
* @author Chill
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper, ProjectApply> implements IProjectApplyService {
|
public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper, ProjectApply> implements IProjectApplyService {
|
||||||
|
|
||||||
@@ -109,6 +112,16 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
|||||||
return projectApplyVO;
|
return projectApplyVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TenantIgnore
|
||||||
|
public ProjectApplyVO publicDetail(Long id) {
|
||||||
|
ProjectApplyVO projectApplyVO = ProjectApplyWrapper.build().entityVO(loadExists(id));
|
||||||
|
normalizeOptionalFields(projectApplyVO);
|
||||||
|
projectApplyVO.setReadonly(true);
|
||||||
|
fillPublicDictOptions(projectApplyVO);
|
||||||
|
return projectApplyVO;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply) {
|
public Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply) {
|
||||||
projectApply.setFundUseRisk(null);
|
projectApply.setFundUseRisk(null);
|
||||||
@@ -878,6 +891,62 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
|||||||
projectApplyVO.setReadonly(!canCurrentUserOperate(projectApplyVO));
|
projectApplyVO.setReadonly(!canCurrentUserOperate(projectApplyVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开页没有登录态,下拉字典接口无法访问,这里按项目租户把文案一并返回。
|
||||||
|
*/
|
||||||
|
private void fillPublicDictOptions(ProjectApplyVO projectApplyVO) {
|
||||||
|
try {
|
||||||
|
projectApplyVO.setCargoTypeOptions(loadSystemDictOptions("type_of_goods"));
|
||||||
|
projectApplyVO.setTransportTypeOptions(loadBizDictOptions(projectApplyVO.getTenantId(), "transport_type"));
|
||||||
|
projectApplyVO.setSettlementModeOptions(loadBizDictOptions(projectApplyVO.getTenantId(), "settle_method"));
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.warn("公开项目详情字典加载失败, id={}", projectApplyVO.getId(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, String>> loadSystemDictOptions(String code) {
|
||||||
|
return toDictOptions(baseMapper.selectDictItems(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, String>> loadBizDictOptions(String tenantId, String code) {
|
||||||
|
if (Func.isEmpty(tenantId)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return toDictOptions(baseMapper.selectDictBizItems(tenantId, code));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, String>> toDictOptions(List<Map<String, Object>> rows) {
|
||||||
|
List<Map<String, String>> options = new ArrayList<>();
|
||||||
|
if (rows == null) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
for (Map<String, Object> row : rows) {
|
||||||
|
String dictKey = dictColumn(row, "dictKey", "dictkey", "dict_key");
|
||||||
|
if (Func.isEmpty(dictKey) || "-1".equals(dictKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String dictValue = dictColumn(row, "dictValue", "dictvalue", "dict_value");
|
||||||
|
Map<String, String> option = new LinkedHashMap<>();
|
||||||
|
option.put("value", dictKey);
|
||||||
|
option.put("label", Func.isEmpty(dictValue) ? dictKey : dictValue);
|
||||||
|
options.add(option);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String dictColumn(Map<String, Object> row, String... names) {
|
||||||
|
if (row == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (String name : names) {
|
||||||
|
Object value = row.get(name);
|
||||||
|
if (value != null && Func.isNotEmpty(String.valueOf(value))) {
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private boolean canCurrentUserOperate(ProjectApply projectApply) {
|
private boolean canCurrentUserOperate(ProjectApply projectApply) {
|
||||||
if (AuthUtil.isAdministrator()) {
|
if (AuthUtil.isAdministrator()) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+280
-6
@@ -198,6 +198,209 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void syncMileageFromWaybill(Long waybillId, BigDecimal mileage) {
|
||||||
|
if (waybillId == null || mileage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Waybill waybill = waybillService.getById(waybillId);
|
||||||
|
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BigDecimal normalizedMileage = normalizeGeneratedMileage(mileage);
|
||||||
|
Set<Long> detailIds = new LinkedHashSet<>();
|
||||||
|
list(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||||
|
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||||
|
.eq(ReceivablePayableDetail::getWaybillId, waybillId)
|
||||||
|
.eq(ReceivablePayableDetail::getSettlementStatus, "pending")
|
||||||
|
.and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getPreSettlementNo)
|
||||||
|
.or().eq(ReceivablePayableDetail::getPreSettlementNo, ""))
|
||||||
|
.and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getFormalSettlementNo)
|
||||||
|
.or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")))
|
||||||
|
.forEach(detail -> detailIds.add(detail.getId()));
|
||||||
|
List<ReceivablePayableCargoFee> relatedFees = cargoFeeMapper.selectList(
|
||||||
|
Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||||
|
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
|
||||||
|
.eq(ReceivablePayableCargoFee::getWaybillId, waybillId));
|
||||||
|
relatedFees.stream()
|
||||||
|
.map(ReceivablePayableCargoFee::getDetailId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.forEach(detailIds::add);
|
||||||
|
if (detailIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Long detailId : detailIds) {
|
||||||
|
ReceivablePayableDetail detail = getById(detailId);
|
||||||
|
if (detail == null || Objects.equals(detail.getIsDeleted(), 1)
|
||||||
|
|| !"pending".equals(detail.getSettlementStatus())
|
||||||
|
|| Func.isNotEmpty(detail.getPreSettlementNo())
|
||||||
|
|| Func.isNotEmpty(detail.getFormalSettlementNo())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
syncDetailMileage(detail, waybill, normalizedMileage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncDetailMileage(ReceivablePayableDetail detail, Waybill waybill, BigDecimal mileage) {
|
||||||
|
List<ReceivablePayableCargoFee> rows = activeCargoFees(detail.getId());
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
if (Objects.equals(detail.getWaybillId(), waybill.getId())) {
|
||||||
|
detail.setMileage(mileage);
|
||||||
|
updateById(detail);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> changes = new ArrayList<>();
|
||||||
|
boolean feeChanged = false;
|
||||||
|
for (ReceivablePayableCargoFee fee : rows) {
|
||||||
|
boolean related = Objects.equals(fee.getWaybillId(), waybill.getId())
|
||||||
|
|| (fee.getWaybillId() == null && Objects.equals(detail.getWaybillId(), waybill.getId()));
|
||||||
|
if (!related) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal oldMileage = normalizeGeneratedMileage(fee.getMileage());
|
||||||
|
fee.setMileage(mileage);
|
||||||
|
if (isMileageBasedElement(fee.getBillingFactor()) && !isManualFee(fee)
|
||||||
|
&& recalculateMileageBasedFee(detail, fee, waybill, oldMileage)) {
|
||||||
|
changes.add("【运单维护里程】按" + fee.getBillingFactor() + "重新计算费用");
|
||||||
|
}
|
||||||
|
cargoFeeMapper.updateById(fee);
|
||||||
|
feeChanged = true;
|
||||||
|
appendChange(changes, "里程", oldMileage, mileage);
|
||||||
|
}
|
||||||
|
if (!feeChanged && !Objects.equals(detail.getWaybillId(), waybill.getId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshAdjustedDetail(detail, activeCargoFees(detail.getId()));
|
||||||
|
if (Objects.equals(detail.getWaybillId(), waybill.getId())) {
|
||||||
|
detail.setMileage(mileage);
|
||||||
|
updateById(detail);
|
||||||
|
}
|
||||||
|
if (!changes.isEmpty()) {
|
||||||
|
saveChangeRecord(detail, String.join(";", changes), "运单维护里程同步", "0001");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按最新运单里程重算「按里程 / 按吨·公里」费用行。
|
||||||
|
*/
|
||||||
|
private boolean recalculateMileageBasedFee(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee,
|
||||||
|
Waybill sourceWaybill, BigDecimal previousMileage) {
|
||||||
|
Waybill calcWaybill = waybillForMileageRecalc(sourceWaybill, fee, previousMileage);
|
||||||
|
List<Map<String, Object>> rules = parseList(fee.getBillingRulesJson());
|
||||||
|
if (rules.isEmpty()) {
|
||||||
|
ContractManage contract = contractManageService.getById(detail.getContractId());
|
||||||
|
if (contract != null) {
|
||||||
|
rules = matchingAdjustedRules(contract, fee, calcWaybill);
|
||||||
|
if (!rules.isEmpty()) {
|
||||||
|
fee.setBillingRulesJson(JsonUtil.toJson(rules));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rules.isEmpty()) {
|
||||||
|
log.warn("运单维护里程重算跳过:未找到计费规则,detailId={}, feeId={}", detail.getId(), fee.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Map<String, BigDecimal> calculatedAmounts = new LinkedHashMap<>();
|
||||||
|
BigDecimal calculatedFreight = BigDecimal.ZERO;
|
||||||
|
boolean freightRuleMatched = false;
|
||||||
|
Map<String, Object> measureRule = null;
|
||||||
|
for (Map<String, Object> rule : rules) {
|
||||||
|
BigDecimal amount = calculateRule(rule, calcWaybill);
|
||||||
|
if (amount == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String feeItem = stringValue(rule, "feeItem", "费用");
|
||||||
|
calculatedAmounts.merge(feeItem, amount, BigDecimal::add);
|
||||||
|
if (measureRule == null || isFreightRule(rule)) {
|
||||||
|
measureRule = rule;
|
||||||
|
}
|
||||||
|
if (isFreightRule(rule)) {
|
||||||
|
calculatedFreight = calculatedFreight.add(amount);
|
||||||
|
freightRuleMatched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (calculatedAmounts.isEmpty() || measureRule == null) {
|
||||||
|
log.warn("运单维护里程重算跳过:计费规则无法试算,detailId={}, feeId={}", detail.getId(), fee.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||||
|
parseMap(fee.getFeeItemsJson()).forEach((name, value) -> feeItems.put(name, decimal(value)));
|
||||||
|
calculatedAmounts.forEach(feeItems::put);
|
||||||
|
fee.setTransportQuantity(resolveStoredTransportQuantity(measureRule, calcWaybill));
|
||||||
|
fee.setUnitPrice(resolveCalculatedUnitPrice(measureRule, calcWaybill));
|
||||||
|
fee.setBillingFactor(stringValue(measureRule, "billingElement", fee.getBillingFactor()));
|
||||||
|
fee.setBillingType(stringValue(measureRule, "billingType", fee.getBillingType()));
|
||||||
|
fee.setPriceUnit(stringValue(measureRule, "billingUnit", fee.getPriceUnit()));
|
||||||
|
if (freightRuleMatched) {
|
||||||
|
fee.setFreightAmount(calculatedFreight);
|
||||||
|
}
|
||||||
|
fee.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||||
|
BigDecimal afterAmount = adjustedAfterAmount(money(fee.getFreightAmount()), feeItems);
|
||||||
|
fee.setAdjustAmount(afterAmount.subtract(money(fee.getOriginalAmount())));
|
||||||
|
fee.setAfterAmount(afterAmount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Waybill waybillForMileageRecalc(Waybill sourceWaybill, ReceivablePayableCargoFee fee,
|
||||||
|
BigDecimal previousMileage) {
|
||||||
|
Waybill calcWaybill = Objects.requireNonNull(BeanUtil.copyProperties(sourceWaybill, Waybill.class));
|
||||||
|
calcWaybill.setMileage(sourceWaybill.getMileage());
|
||||||
|
String element = fee.getBillingFactor();
|
||||||
|
if (!"按吨·公里".equals(element) && !"按重量".equals(element)
|
||||||
|
&& !"按数量".equals(element) && !"按体积".equals(element)) {
|
||||||
|
return calcWaybill;
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> matchedGoods = parseList(sourceWaybill.getGoodsJson()).stream()
|
||||||
|
.filter(goods -> matchesFeeCargo(goods, fee))
|
||||||
|
.toList();
|
||||||
|
if (!matchedGoods.isEmpty()) {
|
||||||
|
Map<String, Object> goods = matchedGoods.get(0);
|
||||||
|
calcWaybill.setGoodsJson(JsonUtil.toJson(List.of(goods)));
|
||||||
|
calcWaybill.setCargoName(stringValue(goods, "cargoName", fee.getCargoName()));
|
||||||
|
calcWaybill.setCargoType(stringValue(goods, "cargoType", fee.getCargoType()));
|
||||||
|
calcWaybill.setSpecification(stringValue(goods, "specification", fee.getSpecification()));
|
||||||
|
calcWaybill.setModel(stringValue(goods, "model", fee.getModel()));
|
||||||
|
calcWaybill.setQuantity(decimal(goods.get("quantity")));
|
||||||
|
calcWaybill.setQuantityUnit(stringValue(goods, "quantityUnit", fee.getQuantityUnit()));
|
||||||
|
return calcWaybill;
|
||||||
|
}
|
||||||
|
Map<String, Object> 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("quantityUnit", fee.getQuantityUnit());
|
||||||
|
BigDecimal quantity = money(fee.getTransportQuantity());
|
||||||
|
if ("按吨·公里".equals(element)) {
|
||||||
|
BigDecimal baseMileage = money(previousMileage);
|
||||||
|
if (baseMileage.signum() > 0) {
|
||||||
|
quantity = quantity.divide(baseMileage, 8, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goods.put("quantity", quantity);
|
||||||
|
if ("按体积".equals(element)) {
|
||||||
|
goods.put("volume", quantity);
|
||||||
|
}
|
||||||
|
calcWaybill.setGoodsJson(JsonUtil.toJson(List.of(goods)));
|
||||||
|
calcWaybill.setCargoName(fee.getCargoName());
|
||||||
|
calcWaybill.setCargoType(fee.getCargoType());
|
||||||
|
calcWaybill.setSpecification(fee.getSpecification());
|
||||||
|
calcWaybill.setModel(fee.getModel());
|
||||||
|
calcWaybill.setQuantity(quantity);
|
||||||
|
calcWaybill.setQuantityUnit(fee.getQuantityUnit());
|
||||||
|
return calcWaybill;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesFeeCargo(Map<String, Object> goods, ReceivablePayableCargoFee fee) {
|
||||||
|
return Objects.equals(stringValue(goods, "cargoName", ""), Objects.toString(fee.getCargoName(), ""))
|
||||||
|
&& Objects.equals(stringValue(goods, "cargoType", ""), Objects.toString(fee.getCargoType(), ""))
|
||||||
|
&& Objects.equals(stringValue(goods, "specification", ""), Objects.toString(fee.getSpecification(), ""))
|
||||||
|
&& Objects.equals(stringValue(goods, "model", ""), Objects.toString(fee.getModel(), ""))
|
||||||
|
&& Objects.equals(stringValue(goods, "quantityUnit", ""), Objects.toString(fee.getQuantityUnit(), ""));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReceivablePayableFeeDetailVO feeDetail(Long id) {
|
public ReceivablePayableFeeDetailVO feeDetail(Long id) {
|
||||||
ReceivablePayableDetail detail = getExisting(id);
|
ReceivablePayableDetail detail = getExisting(id);
|
||||||
@@ -890,11 +1093,68 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
Map<String, Object> plan = resolveDefaultBillingPlan(plans, waybill.getTransportType());
|
Map<String, Object> plan = resolveDefaultBillingPlan(plans, waybill.getTransportType());
|
||||||
if (plan == null) return null;
|
if (plan == null) return null;
|
||||||
if (!(plan.get("rules") instanceof List<?> rules)) return null;
|
if (!(plan.get("rules") instanceof List<?> rules)) return null;
|
||||||
boolean matched = rules.stream().anyMatch(value -> value instanceof Map<?, ?> raw && matchesRule(raw, waybill));
|
boolean matched = rules.stream().anyMatch(value -> {
|
||||||
|
if (!(value instanceof Map<?, ?> raw)) return false;
|
||||||
|
Map<String, Object> rule = toRuleMap(raw);
|
||||||
|
return feeWaybills(rule, waybill).stream()
|
||||||
|
.anyMatch(feeWaybill -> matchesBillingRule(raw, rule, feeWaybill));
|
||||||
|
});
|
||||||
if (!matched) return null;
|
if (!matched) return null;
|
||||||
return "__matched__";
|
return "__matched__";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toRuleMap(Map<?, ?> raw) {
|
||||||
|
Map<String, Object> rule = new LinkedHashMap<>();
|
||||||
|
raw.forEach((key, item) -> rule.put(String.valueOf(key), item));
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 费用生成规则命中:
|
||||||
|
* 1. 货物需满足匹配规则(始发/目的/货类/货名)
|
||||||
|
* 2. 普通计费要素还要求货物数量单位与计费单位一致
|
||||||
|
* 3. 按里程、按吨·公里按运单里程(×重量)另行计算,不校验数量单位
|
||||||
|
*/
|
||||||
|
private boolean matchesBillingRule(Map<?, ?> raw, Map<String, Object> rule, Waybill feeWaybill) {
|
||||||
|
if (!matchesRule(raw, feeWaybill)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String element = stringValue(rule, "billingElement", "");
|
||||||
|
if (isMileageBasedElement(element)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return matchesBillingUnit(rule, feeWaybill);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMileageBasedElement(String element) {
|
||||||
|
return "按里程".equals(element) || "按吨·公里".equals(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesBillingUnit(Map<String, Object> rule, Waybill feeWaybill) {
|
||||||
|
String billingUnit = stringValue(rule, "billingUnit", "").trim();
|
||||||
|
if (billingUnit.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String quantityUnit = resolveQuantityUnit(feeWaybill);
|
||||||
|
if (quantityUnit.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return billingUnit.equals(quantityUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveQuantityUnit(Waybill feeWaybill) {
|
||||||
|
String quantityUnit = feeWaybill.getQuantityUnit() == null ? "" : feeWaybill.getQuantityUnit().trim();
|
||||||
|
if (!quantityUnit.isEmpty()) {
|
||||||
|
return quantityUnit;
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> goods = parseList(feeWaybill.getGoodsJson());
|
||||||
|
return goods.stream()
|
||||||
|
.map(item -> stringValue(item, "quantityUnit", "").trim())
|
||||||
|
.filter(Func::isNotEmpty)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
private boolean matchesRule(Map<?, ?> raw, Waybill waybill) {
|
private boolean matchesRule(Map<?, ?> raw, Waybill waybill) {
|
||||||
Object conditionValue = raw.get("matchCondition");
|
Object conditionValue = raw.get("matchCondition");
|
||||||
if (!(conditionValue instanceof Map<?, ?> condition) || !hasConfiguredMatchCondition(condition)) return true;
|
if (!(conditionValue instanceof Map<?, ?> condition) || !hasConfiguredMatchCondition(condition)) return true;
|
||||||
@@ -1387,10 +1647,10 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
Set<List<String>> freightBillingCargoKeys = new LinkedHashSet<>();
|
Set<List<String>> freightBillingCargoKeys = new LinkedHashSet<>();
|
||||||
for (Object value : (List<?>) plan.get("rules")) {
|
for (Object value : (List<?>) plan.get("rules")) {
|
||||||
if (!(value instanceof Map<?, ?> raw)) continue;
|
if (!(value instanceof Map<?, ?> raw)) continue;
|
||||||
Map<String, Object> rule = new LinkedHashMap<>();
|
Map<String, Object> rule = toRuleMap(raw);
|
||||||
raw.forEach((key, item) -> rule.put(String.valueOf(key), item));
|
|
||||||
for (Waybill feeWaybill : feeWaybills(rule, waybill)) {
|
for (Waybill feeWaybill : feeWaybills(rule, waybill)) {
|
||||||
if (matchOnly && !matchesRule(raw, feeWaybill)) continue;
|
// 统一生成:匹配规则内货物 + 数量单位一致;按里程/按吨·公里跳过单位匹配
|
||||||
|
if (!matchesBillingRule(raw, rule, feeWaybill)) continue;
|
||||||
BigDecimal amount = calculateRule(rule, feeWaybill);
|
BigDecimal amount = calculateRule(rule, feeWaybill);
|
||||||
if (amount == null) continue;
|
if (amount == null) continue;
|
||||||
String feeItem = stringValue(rule, "feeItem", "费用");
|
String feeItem = stringValue(rule, "feeItem", "费用");
|
||||||
@@ -1446,13 +1706,25 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
|
|
||||||
private void fillCalculatedBillingFields(ReceivablePayableCargoFee fee, Waybill feeWaybill,
|
private void fillCalculatedBillingFields(ReceivablePayableCargoFee fee, Waybill feeWaybill,
|
||||||
Map<String, Object> rule) {
|
Map<String, Object> rule) {
|
||||||
fee.setBillingFactor(stringValue(rule, "billingElement", ""));
|
String element = stringValue(rule, "billingElement", "");
|
||||||
|
fee.setBillingFactor(element);
|
||||||
fee.setBillingType(stringValue(rule, "billingType", ""));
|
fee.setBillingType(stringValue(rule, "billingType", ""));
|
||||||
fee.setTransportQuantity(measure(rule, feeWaybill));
|
fee.setTransportQuantity(resolveStoredTransportQuantity(rule, feeWaybill));
|
||||||
fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit()));
|
fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit()));
|
||||||
fee.setUnitPrice(resolveCalculatedUnitPrice(rule, feeWaybill));
|
fee.setUnitPrice(resolveCalculatedUnitPrice(rule, feeWaybill));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 费用行运输量展示值:按车辆/固定金额的计费量仅试算用(为 1),不回写到运输量字段。
|
||||||
|
*/
|
||||||
|
private BigDecimal resolveStoredTransportQuantity(Map<String, Object> rule, Waybill waybill) {
|
||||||
|
String element = stringValue(rule, "billingElement", "");
|
||||||
|
if ("按车辆".equals(element) || "固定金额(整单一口价)".equals(element)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return measure(rule, waybill);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析费用行展示用的实际命中单价。
|
* 解析费用行展示用的实际命中单价。
|
||||||
* 区间计费的规则默认单价仅用于兜底,费用行应展示当前计费量命中的区间单价。
|
* 区间计费的规则默认单价仅用于兜底,费用行应展示当前计费量命中的区间单价。
|
||||||
@@ -1635,7 +1907,9 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
return switch (element) {
|
return switch (element) {
|
||||||
case "按体积" -> goods.stream().map(this::goodsVolume).reduce(BigDecimal.ZERO, BigDecimal::add);
|
case "按体积" -> goods.stream().map(this::goodsVolume).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE;
|
case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE;
|
||||||
|
// 按里程:计费量=运单里程;固定单价/区间单价/阶梯等均基于该计费量
|
||||||
case "按里程" -> money(waybill.getMileage());
|
case "按里程" -> money(waybill.getMileage());
|
||||||
|
// 按吨·公里:计费量=货物重量数量 × 运单里程
|
||||||
case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage()));
|
case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage()));
|
||||||
case "按数量" -> goodsQuantity(goods);
|
case "按数量" -> goodsQuantity(goods);
|
||||||
default -> goodsQuantity(goods);
|
default -> goodsQuantity(goods);
|
||||||
|
|||||||
+3
-6
@@ -43,8 +43,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.format.DateTimeParseException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -64,7 +62,6 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
|
|||||||
private static final int REMARK_MAX_LENGTH = 500;
|
private static final int REMARK_MAX_LENGTH = 500;
|
||||||
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
|
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
|
||||||
private static final int MONEY_SCALE = 2;
|
private static final int MONEY_SCALE = 2;
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) {
|
public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) {
|
||||||
@@ -144,9 +141,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return LocalDate.parse(normalizedValue, DATE_FORMATTER);
|
return org.springblade.common.excel.LenientDateParser.parseDate(normalizedValue, "换胎时间");
|
||||||
} catch (DateTimeParseException exception) {
|
} catch (Exception exception) {
|
||||||
validationErrors.add("换胎时间格式不正确,请使用yyyy-MM-dd格式并填写有效日期");
|
validationErrors.add(exception.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user