✨ 认证退出与操作日志、密码规则,以及运输业务多项修正
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
package org.springblade.auth.config;
|
||||
|
||||
import org.springblade.auth.filter.LogoutLogFilter;
|
||||
import org.springblade.auth.granter.IamAwareTokenGranterFactory;
|
||||
import org.springblade.auth.handler.BladeAuthorizationHandler;
|
||||
import org.springblade.auth.handler.BladeLockHandler;
|
||||
@@ -53,9 +54,11 @@ import org.springblade.core.tenant.BladeTenantProperties;
|
||||
import org.springblade.system.feign.IUserClient;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
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.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -88,6 +91,16 @@ public class BladeAuthConfiguration {
|
||||
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
|
||||
public PasswordHandler passwordHandler(OAuth2Properties 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.oauth2.provider.OAuth2Request;
|
||||
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.Func;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.core.tool.utils.WebUtil;
|
||||
import org.springblade.system.pojo.entity.AuthLog;
|
||||
import org.springblade.system.feign.IAuthLogClient;
|
||||
import org.springblade.system.pojo.entity.AuthLog;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -42,7 +45,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 认证日志处理器
|
||||
* 在用户认证成功时异步记录登录日志
|
||||
* 在用户认证成功/退出时异步记录登录、退出日志
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@@ -50,6 +53,11 @@ import java.util.concurrent.CompletableFuture;
|
||||
@RequiredArgsConstructor
|
||||
public class BladeLogHandler {
|
||||
|
||||
/**
|
||||
* 退出登录授权类型标识
|
||||
*/
|
||||
public static final String GRANT_TYPE_LOGOUT = "logout";
|
||||
|
||||
private final IAuthLogClient authLogClient;
|
||||
private final BladeProperties bladeProperties;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建退出日志实体
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
的 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>
|
||||
<groupId>org.springblade</groupId>
|
||||
<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;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springblade.common.aspect.OperationApiLogAspect;
|
||||
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
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AllArgsConstructor
|
||||
@AutoConfiguration
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+3
@@ -59,6 +59,9 @@ public class CustomerReceiptAccount extends TenantEntity {
|
||||
@Schema(description = "开户行名称")
|
||||
private String bankName;
|
||||
|
||||
@Schema(description = "联行号")
|
||||
private String cnapsCode;
|
||||
|
||||
@Schema(description = "银行账号")
|
||||
private String bankAccount;
|
||||
|
||||
|
||||
+11
-12
@@ -39,6 +39,7 @@ import org.springblade.common.constant.DataStatusEnum;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.springblade.common.constant.ParamConstant;
|
||||
import org.springblade.common.constant.TenantConstant;
|
||||
import org.springblade.common.utils.PasswordRuleUtil;
|
||||
import org.springblade.core.cache.utils.CacheUtil;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
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.transaction.annotation.Transactional;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
@@ -106,9 +106,6 @@ import static org.springblade.core.tenant.TenantGuard.EntityType.USER;
|
||||
@Slf4j
|
||||
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implements IUserService {
|
||||
private static final String GUEST_NAME = "guest";
|
||||
private static final String PASSWORD_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
|
||||
private static final int RANDOM_PASSWORD_LENGTH = 8;
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
private static final String IAM_DEFAULT_DICT_CODE = "iam_default";
|
||||
private static final String IAM_DEFAULT_ROLE_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);
|
||||
Map<String, String> passwordMap = new LinkedHashMap<>();
|
||||
for (User user : userList) {
|
||||
String password = randomPassword();
|
||||
String password = PasswordRuleUtil.generate();
|
||||
User updateUser = new User();
|
||||
updateUser.setPassword(DigestUtil.encrypt(password));
|
||||
updateUser.setUpdateTime(DateUtil.now());
|
||||
@@ -604,6 +601,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
if (!Objects.equals(password, password2)) {
|
||||
throw new ServiceException("两次输入密码不一致!");
|
||||
}
|
||||
validatePlainPassword(password);
|
||||
User user = TenantGuard.verify(this, userId, USER);
|
||||
User updateUser = new User();
|
||||
updateUser.setPassword(DigestUtil.encrypt(password));
|
||||
@@ -672,7 +670,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
if (user.getId() != null) {
|
||||
this.updateUser(user);
|
||||
} else {
|
||||
user.setPassword(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
||||
// 空密码交由 saveUser 按规则填充(初始密码不合规时自动生成)
|
||||
user.setPassword(null);
|
||||
this.submit(user);
|
||||
}
|
||||
});
|
||||
@@ -1064,8 +1063,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
}
|
||||
}
|
||||
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()));
|
||||
Long userCount = baseMapper.selectCount(Wrappers.<User>query().lambda().eq(User::getTenantId, tenantId).eq(User::getAccount, user.getAccount()));
|
||||
if (userCount > 0L && Func.isEmpty(user.getId())) {
|
||||
@@ -1147,12 +1148,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
// }
|
||||
}
|
||||
|
||||
private String randomPassword() {
|
||||
StringBuilder password = new StringBuilder(RANDOM_PASSWORD_LENGTH);
|
||||
for (int index = 0; index < RANDOM_PASSWORD_LENGTH; index++) {
|
||||
password.append(PASSWORD_CHARS.charAt(SECURE_RANDOM.nextInt(PASSWORD_CHARS.length())));
|
||||
private void validatePlainPassword(String password) {
|
||||
if (!PasswordRuleUtil.isValid(password)) {
|
||||
throw new ServiceException(PasswordRuleUtil.RULE_MESSAGE);
|
||||
}
|
||||
return password.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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.ReceivablePayableFeeDetailVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
@@ -82,6 +83,12 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
|
||||
void generateForCompletedWaybills(List<Long> waybillIds);
|
||||
|
||||
/**
|
||||
* 运单维护里程后,同步未挂结算单的应收应付明细里程;
|
||||
* 对按里程、按吨·公里计费的费用行按新里程重新计算。
|
||||
*/
|
||||
void syncMileageFromWaybill(Long waybillId, BigDecimal mileage);
|
||||
|
||||
/**
|
||||
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
|
||||
* <p>与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。</p>
|
||||
|
||||
+4
@@ -657,6 +657,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
if (contractManage.getInvoiceCycle() != null && contractManage.getInvoiceCycle() <= 0) {
|
||||
contractManage.setInvoiceCycle(null);
|
||||
}
|
||||
BigDecimal contractAmount = contractManage.getContractAmount();
|
||||
if (contractAmount != null && contractAmount.compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||
contractManage.setContractAmount(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDraft(ContractManage contractManage) {
|
||||
|
||||
+17
@@ -621,6 +621,23 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
if (Func.isEmpty(accountVO.getAccountName()) && Func.isEmpty(accountVO.getBankAccount())) {
|
||||
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));
|
||||
account.setId(IdWorker.getId());
|
||||
account.setCustomerId(customerId);
|
||||
|
||||
+1
-1
@@ -598,7 +598,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
if (Func.isEmpty(request.getRows())) {
|
||||
throw new ServiceException("请填写需要调整的费用行");
|
||||
}
|
||||
String changeReason = requiredText(limitRemark(request.getChangeReason(), 200), "调整原因");
|
||||
String changeReason = limitRemark(request.getChangeReason(), 200);
|
||||
Map<Long, PreSettlementDetailFee> existingMap = detailFees(detail.getId()).stream()
|
||||
.collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity()));
|
||||
if (request.getRows().size() != existingMap.size()) {
|
||||
|
||||
+280
-6
@@ -198,6 +198,209 @@ public class ReceivablePayableDetailServiceImpl
|
||||
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
|
||||
public ReceivablePayableFeeDetailVO feeDetail(Long id) {
|
||||
ReceivablePayableDetail detail = getExisting(id);
|
||||
@@ -890,11 +1093,68 @@ public class ReceivablePayableDetailServiceImpl
|
||||
Map<String, Object> plan = resolveDefaultBillingPlan(plans, waybill.getTransportType());
|
||||
if (plan == null) return null;
|
||||
if (!(plan.get("rules") instanceof List<?> rules)) return null;
|
||||
boolean matched = rules.stream().anyMatch(value -> value instanceof Map<?, ?> raw && matchesRule(raw, waybill));
|
||||
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;
|
||||
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) {
|
||||
Object conditionValue = raw.get("matchCondition");
|
||||
if (!(conditionValue instanceof Map<?, ?> condition) || !hasConfiguredMatchCondition(condition)) return true;
|
||||
@@ -1387,10 +1647,10 @@ public class ReceivablePayableDetailServiceImpl
|
||||
Set<List<String>> freightBillingCargoKeys = new LinkedHashSet<>();
|
||||
for (Object value : (List<?>) plan.get("rules")) {
|
||||
if (!(value instanceof Map<?, ?> raw)) continue;
|
||||
Map<String, Object> rule = new LinkedHashMap<>();
|
||||
raw.forEach((key, item) -> rule.put(String.valueOf(key), item));
|
||||
Map<String, Object> rule = toRuleMap(raw);
|
||||
for (Waybill feeWaybill : feeWaybills(rule, waybill)) {
|
||||
if (matchOnly && !matchesRule(raw, feeWaybill)) continue;
|
||||
// 统一生成:匹配规则内货物 + 数量单位一致;按里程/按吨·公里跳过单位匹配
|
||||
if (!matchesBillingRule(raw, rule, feeWaybill)) continue;
|
||||
BigDecimal amount = calculateRule(rule, feeWaybill);
|
||||
if (amount == null) continue;
|
||||
String feeItem = stringValue(rule, "feeItem", "费用");
|
||||
@@ -1446,13 +1706,25 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
private void fillCalculatedBillingFields(ReceivablePayableCargoFee fee, Waybill feeWaybill,
|
||||
Map<String, Object> rule) {
|
||||
fee.setBillingFactor(stringValue(rule, "billingElement", ""));
|
||||
String element = stringValue(rule, "billingElement", "");
|
||||
fee.setBillingFactor(element);
|
||||
fee.setBillingType(stringValue(rule, "billingType", ""));
|
||||
fee.setTransportQuantity(measure(rule, feeWaybill));
|
||||
fee.setTransportQuantity(resolveStoredTransportQuantity(rule, feeWaybill));
|
||||
fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit()));
|
||||
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) {
|
||||
case "按体积" -> goods.stream().map(this::goodsVolume).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE;
|
||||
// 按里程:计费量=运单里程;固定单价/区间单价/阶梯等均基于该计费量
|
||||
case "按里程" -> money(waybill.getMileage());
|
||||
// 按吨·公里:计费量=货物重量数量 × 运单里程
|
||||
case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage()));
|
||||
case "按数量" -> goodsQuantity(goods);
|
||||
default -> goodsQuantity(goods);
|
||||
|
||||
+5
-1
@@ -1045,7 +1045,11 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
TransportBusinessSupport.validateLength(mileageRemark, 200, "里程维护备注不能超过200字");
|
||||
waybill.setMileage(mileage);
|
||||
waybill.setMileageRemark(mileageRemark);
|
||||
return updateById(waybill);
|
||||
boolean updated = updateById(waybill);
|
||||
if (updated) {
|
||||
receivablePayableDetailService.syncMileageFromWaybill(waybill.getId(), mileage);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
@@ -27,6 +27,7 @@ import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.vo.ContractManageVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -47,6 +48,10 @@ public class ContractManageWrapper extends BaseEntityWrapper<ContractManage, Con
|
||||
contractManageVO.setUpdateUserName(org.springblade.system.cache.UserCache.getUserRealName(contractManage.getUpdateUser()));
|
||||
contractManageVO.setContractStageName(stageName(contractManage.getContractStage()));
|
||||
contractManageVO.setApprovalStatusName(statusName(contractManage.getApprovalStatus()));
|
||||
if (contractManageVO.getContractAmount() != null
|
||||
&& contractManageVO.getContractAmount().compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||
contractManageVO.setContractAmount(null);
|
||||
}
|
||||
return contractManageVO;
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -32,6 +32,7 @@ import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -58,9 +59,23 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
|
||||
waybillVO.setBusinessStatus(displayStatus);
|
||||
waybillVO.setBusinessStatusName(businessStatusName(displayStatus));
|
||||
waybillVO.setRequireAccept(WaybillProcessSupport.requiresDriverAcceptConfirmation(waybill.getProcessJson()));
|
||||
// 未维护里程等占位值 -1 对外展示为空
|
||||
if (isSentinelMinusOne(waybillVO.getMileage())) {
|
||||
waybillVO.setMileage(null);
|
||||
}
|
||||
if (isSentinelMinusOne(waybillVO.getQuantity())) {
|
||||
waybillVO.setQuantity(null);
|
||||
}
|
||||
if (isSentinelMinusOne(waybillVO.getUnitPrice())) {
|
||||
waybillVO.setUnitPrice(null);
|
||||
}
|
||||
return waybillVO;
|
||||
}
|
||||
|
||||
private static boolean isSentinelMinusOne(BigDecimal value) {
|
||||
return value != null && value.compareTo(BigDecimal.valueOf(-1)) == 0;
|
||||
}
|
||||
|
||||
public static String businessStatusName(String status) {
|
||||
if (status == null) {
|
||||
return "未知";
|
||||
|
||||
@@ -197,6 +197,9 @@ blade:
|
||||
- /wechat/**
|
||||
#开启错误日志入库
|
||||
error-log: true
|
||||
#增删改操作日志自动入库(blade_log_api)
|
||||
operation:
|
||||
enabled: true
|
||||
#xss配置
|
||||
xss:
|
||||
enabled: true
|
||||
|
||||
@@ -93,6 +93,7 @@ CREATE TABLE `blade_customer_receipt_account` (
|
||||
`account_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款方名称',
|
||||
`account_holder_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开户人姓名',
|
||||
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开户行名称',
|
||||
`cnaps_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联行号',
|
||||
`bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号',
|
||||
`registered_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册电话',
|
||||
`registered_address` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册地址',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 客商收款信息:新增联行号
|
||||
|
||||
ALTER TABLE `blade_customer_receipt_account`
|
||||
ADD COLUMN `cnaps_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联行号' AFTER `bank_name`;
|
||||
Reference in New Issue
Block a user