Merge remote-tracking branch 'origin/master' into feature/payment-apply-bfi-sync
# Conflicts: # blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java # blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java # blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java # blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java # blade-service/blade-transport/pom.xml # blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java # blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java # blade-service/blade-transport/src/main/resources/application-dev.yml # blade-third-party-api/pom.xml # doc/nacos/blade-dev.yaml # doc/nacos/blade-prod.yaml # doc/nacos/third-party-api.yaml # pom.xml
This commit is contained in:
@@ -63,6 +63,10 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-user-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-dict-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-system-api</artifactId>
|
||||
@@ -71,6 +75,10 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-mk-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-wechat-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-resource-api</artifactId>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -32,4 +32,10 @@ package org.springblade.auth.constant;
|
||||
*/
|
||||
public interface BladeAuthConstant {
|
||||
|
||||
/**
|
||||
* 小程序/登录短信验证码资源编号(对应后台 /resource/sms 的 smsCode)
|
||||
*/
|
||||
String LOGIN_SMS_CODE = "ali_reg";
|
||||
|
||||
}
|
||||
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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.endpoint;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint;
|
||||
import org.springblade.core.secure.BladeUser;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 增强 /oauth/user-info 响应,补充 permission 权限标识字段
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestControllerAdvice(assignableTypes = OAuth2TokenEndPoint.class)
|
||||
public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private final ISysClient sysClient;
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return returnType.getMethod() != null && "userInfo".equals(returnType.getMethod().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request, ServerHttpResponse response) {
|
||||
if (!(body instanceof BladeUser bladeUser)) {
|
||||
return body;
|
||||
}
|
||||
OAuth2UserInfoVO userInfo = BeanUtil.copyProperties(bladeUser, OAuth2UserInfoVO.class);
|
||||
if (userInfo == null) {
|
||||
return body;
|
||||
}
|
||||
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
private List<String> loadPermission(String roleId) {
|
||||
if (Func.isBlank(roleId)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
R<List<String>> result = sysClient.getPermissions(roleId);
|
||||
if (result != null && result.isSuccess() && result.getData() != null) {
|
||||
return result.getData();
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
log.warn("加载用户权限标识失败, roleId={}", roleId, exception);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.endpoint;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.secure.BladeUser;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OAuth用户信息(含权限标识,供小程序等客户端使用)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "OAuth用户信息")
|
||||
public class OAuth2UserInfoVO extends BladeUser {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 权限标识集合(菜单按钮 code)
|
||||
*/
|
||||
@Schema(description = "权限标识集合")
|
||||
private List<String> permission;
|
||||
|
||||
}
|
||||
@@ -28,13 +28,14 @@ package org.springblade.auth.endpoint;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springblade.auth.constant.BladeAuthConstant;
|
||||
import org.springblade.core.oauth2.props.OAuth2Properties;
|
||||
import org.springblade.core.oauth2.provider.OAuth2Request;
|
||||
import org.springblade.core.oauth2.service.OAuth2User;
|
||||
import org.springblade.core.oauth2.service.OAuth2UserService;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.SM2Util;
|
||||
import org.springblade.core.tool.utils.StringPool;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.resource.feign.ISmsClient;
|
||||
import org.springblade.resource.utils.SmsUtil;
|
||||
@@ -73,11 +74,14 @@ public class Oauth2SmsEndpoint {
|
||||
* 短信验证码发送
|
||||
*
|
||||
* @param tenantId 租户ID
|
||||
* @param phone 手机号
|
||||
* @param phone 手机号(SM2 加密)
|
||||
* @param code 短信资源编号,默认 ali_reg(后台 /resource/sms)
|
||||
*/
|
||||
@SneakyThrows
|
||||
@PostMapping("/oauth/sms/send-validate")
|
||||
public R sendValidate(@RequestParam String tenantId, @RequestParam String phone) {
|
||||
public R sendValidate(@RequestParam String tenantId,
|
||||
@RequestParam String phone,
|
||||
@RequestParam(required = false) String code) {
|
||||
// 校验手机加密认证,防止恶意发送验证码
|
||||
String decryptedPhone = SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey());
|
||||
if (StringUtil.isBlank(decryptedPhone)) {
|
||||
@@ -90,8 +94,9 @@ public class Oauth2SmsEndpoint {
|
||||
if (oAuth2User == null) {
|
||||
return R.fail(USER_PHONE_NOT_FOUND);
|
||||
}
|
||||
// 用户存在则发送验证码
|
||||
R result = smsClient.sendValidate(tenantId, StringPool.EMPTY, decryptedPhone);
|
||||
// 使用指定短信资源(默认 ali_reg)
|
||||
String smsResourceCode = Func.toStr(code, BladeAuthConstant.LOGIN_SMS_CODE);
|
||||
R result = smsClient.sendValidate(tenantId, smsResourceCode, decryptedPhone);
|
||||
return result.isSuccess() ? R.data(result.getData(), SmsUtil.SEND_SUCCESS) : R.fail(SmsUtil.SEND_FAIL);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import org.springblade.core.redis.cache.BladeRedis;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.core.tool.utils.StringPool;
|
||||
import org.springblade.system.cache.DictCache;
|
||||
import org.springblade.system.feign.IUserClient;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.system.pojo.entity.UserInfo;
|
||||
@@ -84,8 +85,10 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
private static final String GRANT_TYPE = "iam_sso";
|
||||
private static final String IAM_GRANT_TYPE = "authorization_code";
|
||||
private static final String IAM_TOKEN_URI = "/iam/sso/token";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
private static final String BASIC_PREFIX = "Basic ";
|
||||
private static final String IAM_DEFAULT_DICT_CODE = "iam_default";
|
||||
private static final String IAM_DEFAULT_ROLE_NAME = "默认角色";
|
||||
private static final String IAM_DEFAULT_DEPT_NAME = "默认部门";
|
||||
|
||||
private final OAuth2ClientService clientService;
|
||||
private final IUserClient userClient;
|
||||
@@ -154,32 +157,46 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
log.warn("IAM统一身份认证请求缺少租户ID,accountNo={}", accountNo);
|
||||
throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
|
||||
}
|
||||
log.info("IAM统一身份认证开始匹配本系统账号,tenantId={}, accountNo={}", tenantId, accountNo);
|
||||
UserInfo userInfo = loadOrCreateIamUser(request, profileResponse, tenantId, accountNo);
|
||||
OAuth2User user = TokenUtil.convertUser(userInfo, request);
|
||||
if (user == null) {
|
||||
throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
|
||||
}
|
||||
if (user.getAuthorities() == null || user.getAuthorities().isEmpty()) {
|
||||
log.warn("IAM统一身份认证用户缺少可登录角色,tenantId={}, accountNo={}", tenantId, accountNo);
|
||||
throw new UserInvalidException(OAuth2TokenConstant.USER_HAS_NO_ROLE);
|
||||
}
|
||||
user.setClient(client(request));
|
||||
return user;
|
||||
}
|
||||
|
||||
private UserInfo loadOrCreateIamUser(OAuth2Request request, IamSsoProfileResponse profileResponse, String tenantId, String accountNo) {
|
||||
R<UserInfo> result = userClient.userInfo(tenantId, accountNo);
|
||||
if (result.isSuccess() && hasUser(result.getData())) {
|
||||
if (result.isSuccess() && hasLoginAccess(result.getData())) {
|
||||
return result.getData();
|
||||
}
|
||||
log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}",
|
||||
tenantId, accountNo, result.isSuccess());
|
||||
if (result.isSuccess() && hasUser(result.getData())) {
|
||||
log.info("IAM统一身份认证账号缺少有效角色或部门,开始补齐默认配置,tenantId={}, accountNo={}", tenantId, accountNo);
|
||||
} else {
|
||||
log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}",
|
||||
tenantId, accountNo, result.isSuccess());
|
||||
}
|
||||
R<Boolean> saveResult = userClient.saveIamUser(buildIamUser(profileResponse, tenantId, accountNo));
|
||||
if (!saveResult.isSuccess() || !Boolean.TRUE.equals(saveResult.getData())) {
|
||||
log.warn("IAM统一身份认证自动创建用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, saveResult.getMsg());
|
||||
throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
|
||||
String failMsg = StringUtil.isNotBlank(saveResult.getMsg()) ? saveResult.getMsg() : OAuth2TokenConstant.USER_HAS_NO_ROLE;
|
||||
log.warn("IAM统一身份认证自动创建或补齐用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, failMsg);
|
||||
throw new UserInvalidException(failMsg);
|
||||
}
|
||||
R<UserInfo> createdResult = userClient.userInfo(tenantId, accountNo);
|
||||
if (!createdResult.isSuccess() || !hasUser(createdResult.getData())) {
|
||||
log.warn("IAM统一身份认证自动创建用户后未查询到用户,tenantId={}, accountNo={}", tenantId, accountNo);
|
||||
throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
|
||||
}
|
||||
if (!hasRoles(createdResult.getData())) {
|
||||
log.warn("IAM统一身份认证自动创建用户后仍缺少可登录角色,tenantId={}, accountNo={}", tenantId, accountNo);
|
||||
throw new UserInvalidException(OAuth2TokenConstant.USER_HAS_NO_ROLE);
|
||||
}
|
||||
return createdResult.getData();
|
||||
}
|
||||
|
||||
@@ -191,6 +208,21 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
return userInfo != null && userInfo.getUser() != null;
|
||||
}
|
||||
|
||||
private boolean hasRoles(UserInfo userInfo) {
|
||||
return userInfo != null && userInfo.getRoles() != null && !userInfo.getRoles().isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasLoginAccess(UserInfo userInfo) {
|
||||
return hasUser(userInfo) && hasRoles(userInfo) && hasValidAssignment(userInfo.getUser());
|
||||
}
|
||||
|
||||
private boolean hasValidAssignment(User user) {
|
||||
return StringUtil.isNotBlank(user.getRoleId())
|
||||
&& !StringPool.MINUS_ONE.equals(user.getRoleId())
|
||||
&& StringUtil.isNotBlank(user.getDeptId())
|
||||
&& !StringPool.MINUS_ONE.equals(user.getDeptId());
|
||||
}
|
||||
|
||||
private User buildIamUser(IamSsoProfileResponse profileResponse, String tenantId, String accountNo) {
|
||||
User user = new User();
|
||||
user.setTenantId(tenantId);
|
||||
@@ -199,8 +231,8 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
user.setPassword(UUID.randomUUID().toString());
|
||||
user.setName(accountNo);
|
||||
user.setRealName(accountNo);
|
||||
user.setRoleId(StringPool.MINUS_ONE);
|
||||
user.setDeptId(StringPool.MINUS_ONE);
|
||||
user.setRoleId(resolveIamDefaultId(IAM_DEFAULT_ROLE_NAME));
|
||||
user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
|
||||
user.setPostId(StringPool.MINUS_ONE);
|
||||
user.setIsOa(1);
|
||||
user.setSyncTime(new Date());
|
||||
@@ -209,6 +241,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
return user;
|
||||
}
|
||||
|
||||
private String resolveIamDefaultId(String dictValue) {
|
||||
String dictKey = DictCache.getKey(IAM_DEFAULT_DICT_CODE, dictValue);
|
||||
if (StringUtil.isBlank(dictKey) || StringPool.MINUS_ONE.equals(dictKey)) {
|
||||
throw new UserInvalidException(StringUtil.format("IAM默认配置 [{}] 未配置有效键值", dictValue));
|
||||
}
|
||||
return dictKey;
|
||||
}
|
||||
|
||||
public boolean supports(OAuth2Request request) {
|
||||
return isIamRequest(request, false);
|
||||
}
|
||||
@@ -298,12 +338,16 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
|
||||
private IamSsoProfileResponse requestIamProfile(String accessToken) {
|
||||
try {
|
||||
HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(buildProfileUrl(accessToken)))
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(buildProfileUrl(accessToken)))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
|
||||
.header(HttpHeaders.AUTHORIZATION, profileAuthorizationHeader(accessToken))
|
||||
.GET()
|
||||
.build();
|
||||
// 与 IAM 同步接口一致:Authorization 走网关凭证,Auth 走业务凭证。
|
||||
.header(HttpHeaders.AUTHORIZATION, authorizationHeader());
|
||||
String profileAuthHeader = profileAuthHeader();
|
||||
if (StringUtil.isNotBlank(profileAuthHeader)) {
|
||||
requestBuilder.header("Auth", profileAuthHeader);
|
||||
}
|
||||
HttpRequest httpRequest = requestBuilder.GET().build();
|
||||
log.info("IAM统一身份认证获取用户信息请求,url={}, method={}", properties.getProfileUrl(), httpRequest.method());
|
||||
HttpResponse<String> response = httpClient.send(
|
||||
httpRequest,
|
||||
@@ -311,7 +355,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
);
|
||||
log.info("IAM统一身份认证获取用户信息响应,status={}", response.statusCode());
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
log.warn("IAM统一身份认证获取用户信息失败,status={}", response.statusCode());
|
||||
log.warn("IAM统一身份认证获取用户信息失败,status={}, body={}", response.statusCode(), response.body());
|
||||
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
|
||||
}
|
||||
return objectMapper.readValue(response.body(), IamSsoProfileResponse.class);
|
||||
@@ -379,11 +423,15 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter {
|
||||
return BASIC_PREFIX + authorization;
|
||||
}
|
||||
|
||||
private String profileAuthorizationHeader(String accessToken) {
|
||||
if (StringUtil.isNotBlank(properties.getProfileAuthorization())) {
|
||||
return withBasicPrefix(properties.getProfileAuthorization());
|
||||
/**
|
||||
* IAM 业务侧 Auth 头。网关 Authorization 使用 {@link #authorizationHeader()},
|
||||
* 用户令牌通过 query 参数 access_token 传递。
|
||||
*/
|
||||
private String profileAuthHeader() {
|
||||
if (StringUtil.isBlank(properties.getProfileAuthorization())) {
|
||||
return null;
|
||||
}
|
||||
return BEARER_PREFIX + accessToken;
|
||||
return withBasicPrefix(properties.getProfileAuthorization());
|
||||
}
|
||||
|
||||
private String encode(String value) {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
package org.springblade.auth.granter;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springblade.auth.constant.BladeAuthConstant;
|
||||
import org.springblade.core.oauth2.constant.OAuth2TokenConstant;
|
||||
import org.springblade.core.oauth2.exception.UserInvalidException;
|
||||
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
|
||||
@@ -37,8 +38,8 @@ import org.springblade.core.oauth2.service.OAuth2User;
|
||||
import org.springblade.core.oauth2.service.OAuth2UserService;
|
||||
import org.springblade.core.sms.model.SmsCode;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.SM2Util;
|
||||
import org.springblade.core.tool.utils.StringPool;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.core.tool.utils.WebUtil;
|
||||
import org.springblade.resource.feign.ISmsClient;
|
||||
@@ -78,8 +79,10 @@ public class SmsTokenGranter extends AbstractTokenGranter {
|
||||
if (StringUtil.isBlank(decryptedPhone)) {
|
||||
throw new UserInvalidException(OAuth2TokenConstant.USER_PHONE_NOT_FOUND);
|
||||
}
|
||||
// 获取短信验证信息
|
||||
R result = smsClient.validateMessage(tenantId, StringPool.EMPTY, smsCode.getId(), smsCode.getValue(), decryptedPhone);
|
||||
// 与发送时使用同一短信资源编号(默认 ali_reg)
|
||||
HttpServletRequest httpRequest = WebUtil.getRequest();
|
||||
String smsResourceCode = Func.toStr(httpRequest.getParameter("code"), BladeAuthConstant.LOGIN_SMS_CODE);
|
||||
R result = smsClient.validateMessage(tenantId, smsResourceCode, smsCode.getId(), smsCode.getValue(), decryptedPhone);
|
||||
if (!result.isSuccess()) {
|
||||
throw new UserInvalidException(OAuth2TokenConstant.CAPTCHA_NOT_CORRECT);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package org.springblade.auth.granter;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.oauth2.exception.UserInvalidException;
|
||||
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
|
||||
import org.springblade.core.oauth2.handler.PasswordHandler;
|
||||
import org.springblade.core.oauth2.provider.OAuth2Request;
|
||||
import org.springblade.core.oauth2.service.OAuth2ClientService;
|
||||
import org.springblade.core.oauth2.service.OAuth2User;
|
||||
import org.springblade.core.oauth2.service.OAuth2UserService;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.core.tool.utils.WebUtil;
|
||||
import org.springblade.system.feign.IUserClient;
|
||||
import org.springblade.thirdparty.wechat.constant.WechatMiniConstant;
|
||||
import org.springblade.thirdparty.wechat.exception.WechatMiniException;
|
||||
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
|
||||
import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO;
|
||||
import org.springblade.thirdparty.wechat.service.IWechatMiniService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号一键登录。
|
||||
* <p>
|
||||
* grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。
|
||||
* 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WechatMiniTokenGranter extends AbstractTokenGranter {
|
||||
|
||||
private final OAuth2UserService userService;
|
||||
private final IWechatMiniService wechatMiniService;
|
||||
private final IUserClient userClient;
|
||||
|
||||
public WechatMiniTokenGranter(OAuth2ClientService clientService,
|
||||
OAuth2UserService userService,
|
||||
PasswordHandler passwordHandler,
|
||||
IWechatMiniService wechatMiniService,
|
||||
IUserClient userClient) {
|
||||
super(clientService, userService, passwordHandler);
|
||||
this.userService = userService;
|
||||
this.wechatMiniService = wechatMiniService;
|
||||
this.userClient = userClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return WechatMiniConstant.GRANT_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2User user(OAuth2Request request) {
|
||||
// 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败
|
||||
var oauthClient = client(request);
|
||||
|
||||
HttpServletRequest httpRequest = WebUtil.getRequest();
|
||||
String loginCode = httpRequest.getParameter("loginCode");
|
||||
String phoneCode = httpRequest.getParameter("phoneCode");
|
||||
if (StringUtil.isBlank(loginCode) || StringUtil.isBlank(phoneCode)) {
|
||||
throw new UserInvalidException("微信登录参数不完整");
|
||||
}
|
||||
|
||||
WechatSessionVO session;
|
||||
WechatPhoneVO phoneInfo;
|
||||
try {
|
||||
session = wechatMiniService.code2Session(loginCode);
|
||||
phoneInfo = wechatMiniService.getPhoneNumber(phoneCode);
|
||||
} catch (WechatMiniException e) {
|
||||
log.warn("微信小程序登录失败: {}", e.getMessage());
|
||||
throw new UserInvalidException(e.getMessage());
|
||||
}
|
||||
|
||||
String phone = Func.toStr(phoneInfo.getPurePhoneNumber(), phoneInfo.getPhoneNumber());
|
||||
if (StringUtil.isBlank(phone)) {
|
||||
throw new UserInvalidException("未获取到微信手机号");
|
||||
}
|
||||
|
||||
OAuth2User user = userService.loadByPhone(phone, request);
|
||||
if (!userService.validateUser(user)) {
|
||||
throw new UserInvalidException("用户不存在,无法登录");
|
||||
}
|
||||
|
||||
R<Boolean> bindResult = userClient.bindWxMiniOpenId(
|
||||
request.getTenantId(),
|
||||
Func.toLong(user.getUserId()),
|
||||
session.getOpenid(),
|
||||
phone
|
||||
);
|
||||
if (bindResult == null || !bindResult.isSuccess()) {
|
||||
String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败";
|
||||
log.warn("绑定微信 openid 失败 userId={} openid={} msg={}", user.getUserId(), session.getOpenid(), msg);
|
||||
throw new UserInvalidException(StringUtil.isBlank(msg) ? "绑定 openid 失败" : msg);
|
||||
}
|
||||
|
||||
user.setClient(oauthClient);
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
+5
-1
@@ -154,7 +154,11 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
|
||||
*/
|
||||
@Override
|
||||
public void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation) {
|
||||
// 自定义认证失败回调
|
||||
log.error("用户:{},认证失败,失败原因:{},grantType={},authorities={}",
|
||||
user == null ? request.getUsername() : user.getAccount(),
|
||||
validation.getMessage(),
|
||||
request.getGrantType(),
|
||||
user == null ? null : user.getAuthorities());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ public class IamSsoProperties {
|
||||
private String authorization;
|
||||
|
||||
/**
|
||||
* IAM用户信息认证头。为空时默认使用 Bearer accessToken
|
||||
* IAM业务侧 Auth 请求头。为空时不传 Auth;
|
||||
* Authorization 统一使用网关凭证 {@link #authorization}。
|
||||
*/
|
||||
private String profileAuthorization;
|
||||
|
||||
|
||||
+14
-1
@@ -25,11 +25,17 @@
|
||||
*/
|
||||
package org.springblade.auth.service;
|
||||
|
||||
import org.springblade.core.oauth2.constant.OAuth2GranterConstant;
|
||||
import org.springblade.core.oauth2.provider.OAuth2Request;
|
||||
import org.springblade.core.oauth2.service.OAuth2Client;
|
||||
import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.StringPool;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* BladeClientDetailService
|
||||
*
|
||||
@@ -57,6 +63,13 @@ public class BladeClientDetailService extends OAuth2ClientDetailService {
|
||||
|
||||
@Override
|
||||
public boolean validateGranter(OAuth2Client client, String grantType) {
|
||||
return super.validateGranter(client, grantType);
|
||||
// 微信小程序一键登录:兼容库表未配置 wechat_applet 的存量客户端
|
||||
if (OAuth2GranterConstant.WECHAT_APPLET.equals(grantType) || "wechat_mini".equals(grantType)) {
|
||||
return true;
|
||||
}
|
||||
return Optional.ofNullable(client)
|
||||
.map(c -> Arrays.stream(Func.split(c.getAuthorizedGrantTypes(), StringPool.COMMA))
|
||||
.anyMatch(s -> s.trim().equals(grantType)))
|
||||
.orElse(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# 本机多网卡时 Nacos 易误选 169.254.*(链路本地),导致网关 Host is down
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
ip: ${NACOS_DISCOVERY_IP:127.0.0.1}
|
||||
@@ -101,4 +101,4 @@ iam:
|
||||
system-client-secret: ${IAM_SSO_SYSTEM_CLIENT_SECRET:saber3_secret}
|
||||
redirect-uri: ${IAM_SSO_REDIRECT_URI:http://172.16.203.228:8000/callback}
|
||||
authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}
|
||||
profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}
|
||||
profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==}
|
||||
|
||||
@@ -22,6 +22,25 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-starter-loadbalancer</artifactId>
|
||||
</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>
|
||||
<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
-2
@@ -26,6 +26,12 @@
|
||||
package org.springblade.common.config;
|
||||
|
||||
|
||||
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;
|
||||
import feign.Logger;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.common.feign.CustomFeignLogger;
|
||||
@@ -39,10 +45,17 @@ import org.springframework.context.annotation.Primary;
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnClass(name = "feign.Logger")
|
||||
|
||||
+133
-5
@@ -46,9 +46,11 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 导入失败明细 Excel 导出工具类
|
||||
@@ -95,7 +97,7 @@ public class ImportFailureExcelUtil {
|
||||
* @param excelClass 原导入 Excel 类型
|
||||
*/
|
||||
public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) {
|
||||
exportFailureReasonOnly(response, fileName, sheetName, data, excelClass);
|
||||
export(response, fileName, sheetName, data, excelClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +111,16 @@ public class ImportFailureExcelUtil {
|
||||
*/
|
||||
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
|
||||
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.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
||||
@@ -118,7 +130,7 @@ public class ImportFailureExcelUtil {
|
||||
List<List<Object>> rows = buildRows(data, excelFields);
|
||||
try {
|
||||
FastExcel.write(response.getOutputStream())
|
||||
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields.size()))
|
||||
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows, markErrorColumns))
|
||||
.head(head)
|
||||
.sheet(sheetName)
|
||||
.doWrite(rows);
|
||||
@@ -204,15 +216,59 @@ public class ImportFailureExcelUtil {
|
||||
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 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> noWrapStyleCache = new HashMap<>();
|
||||
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
|
||||
|
||||
private ImportFailureCellStyleHandler(int failureReasonColumnIndex) {
|
||||
this.failureReasonColumnIndex = failureReasonColumnIndex;
|
||||
private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows, boolean markErrorColumns) {
|
||||
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
|
||||
@@ -230,11 +286,83 @@ public class ImportFailureExcelUtil {
|
||||
return;
|
||||
}
|
||||
adjustColumnWidth(cell);
|
||||
if (cell.getColumnIndex() == failureReasonColumnIndex) {
|
||||
if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) {
|
||||
return;
|
||||
}
|
||||
if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
|
||||
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) {
|
||||
CellStyle currentStyle = cell.getCellStyle();
|
||||
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
|
||||
@@ -48,6 +48,7 @@ public class AuthProvider {
|
||||
DEFAULT_SKIP_URL.add("/oauth/sms/**");
|
||||
DEFAULT_SKIP_URL.add("/oauth/clear-cache/**");
|
||||
DEFAULT_SKIP_URL.add("/oauth/user-info");
|
||||
DEFAULT_SKIP_URL.add("/oauth/logout/**");
|
||||
DEFAULT_SKIP_URL.add("/oauth/render/**");
|
||||
DEFAULT_SKIP_URL.add("/oauth/callback/**");
|
||||
DEFAULT_SKIP_URL.add("/oauth/revoke/**");
|
||||
@@ -62,6 +63,10 @@ public class AuthProvider {
|
||||
DEFAULT_SKIP_URL.add("/manager/check-upload");
|
||||
DEFAULT_SKIP_URL.add("/assets/**");
|
||||
DEFAULT_SKIP_URL.add("/iam/sso/token/**");
|
||||
DEFAULT_SKIP_URL.add("/blade-transport/customer-archive/public/**");
|
||||
DEFAULT_SKIP_URL.add("/customer-archive/public/**");
|
||||
DEFAULT_SKIP_URL.add("/blade-openapi/openApi/mk/process/commonCallback");
|
||||
DEFAULT_SKIP_URL.add("/openApi/mk/process/commonCallback");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,8 @@ blade:
|
||||
enabled: true
|
||||
#灰度版本
|
||||
#version: 3.0.0
|
||||
#负载均衡优先调用的ip段
|
||||
#负载均衡优先调用的ip段(含本机局域网段,避免只剩 169.254.* 时误路由)
|
||||
prior-ip-pattern:
|
||||
- 192.168.0.*
|
||||
- 192.168.*.*
|
||||
- 172.16.*.*
|
||||
- 127.0.0.1
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#数据源配置
|
||||
# 本机多网卡时 Nacos 易误选 169.254.*(链路本地),导致网关 Host is down
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
ip: ${NACOS_DISCOVERY_IP:127.0.0.1}
|
||||
#数据源配置
|
||||
datasource:
|
||||
url: ${blade.datasource.dev.url}
|
||||
username: ${blade.datasource.dev.username}
|
||||
|
||||
+24
@@ -38,6 +38,8 @@ public interface IBusinessProcessClient {
|
||||
String QUERY_TODO_LIST = API_PREFIX + "/queryTodoList";
|
||||
String QUERY_BUSINESS_PROCESS_SNAPSHOT = API_PREFIX + "/queryBusinessProcessSnapshot";
|
||||
String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments";
|
||||
String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes";
|
||||
String GET_PROCESS_INFO = API_PREFIX + "/getProcessInfo";
|
||||
|
||||
/**
|
||||
* 提交业务流程
|
||||
@@ -102,4 +104,26 @@ public interface IBusinessProcessClient {
|
||||
*/
|
||||
@GetMapping(QUERY_APPROVED_RECORD_LIST)
|
||||
FR<List<ProcessApprovedRecordVO>> queryApprovedRecordsNoAttachments(@RequestParam(name = "bizId", required = false) String bizId, @RequestParam(name = "processInstanceId", required = false) String processInstanceId);
|
||||
|
||||
/**
|
||||
* 获取流程当前节点详情
|
||||
*
|
||||
* @param processInstanceId 流程实例id
|
||||
* @param loginName MK登录名(手机号)
|
||||
* @return 当前节点详情
|
||||
*/
|
||||
@GetMapping(GET_CURRENT_NODES)
|
||||
FR<Object> getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId,
|
||||
@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);
|
||||
}
|
||||
|
||||
+10
@@ -73,6 +73,7 @@ public interface ISysClient {
|
||||
String REGION = API_PREFIX + "/region";
|
||||
String FEE_ITEMS = API_PREFIX + "/fee-items";
|
||||
String CARGO_TYPES = API_PREFIX + "/cargo-types";
|
||||
String PERMISSIONS = API_PREFIX + "/permissions";
|
||||
String INVOICE_ITEMS = API_PREFIX + "/invoice-items";
|
||||
|
||||
/**
|
||||
@@ -317,6 +318,15 @@ public interface ISysClient {
|
||||
@GetMapping(CARGO_TYPES)
|
||||
R<List<CargoType>> getCargoTypes();
|
||||
|
||||
/**
|
||||
* 获取角色权限标识集合(按钮编号)
|
||||
*
|
||||
* @param roleId 角色id
|
||||
* @return 权限标识
|
||||
*/
|
||||
@GetMapping(PERMISSIONS)
|
||||
R<List<String>> getPermissions(@RequestParam("roleId") String roleId);
|
||||
|
||||
/**
|
||||
* 获取启用的开票项目(含税收分类编码,开票申请推送金蝶按分类+简称匹配取数)
|
||||
*
|
||||
|
||||
+5
@@ -174,6 +174,11 @@ public class ISysClientFallback implements ISysClient {
|
||||
return R.fail("获取数据失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<String>> getPermissions(String roleId) {
|
||||
return R.fail("获取数据失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<InvoiceItem>> getInvoiceItems() {
|
||||
return R.fail("获取数据失败");
|
||||
|
||||
+19
@@ -25,10 +25,13 @@
|
||||
*/
|
||||
package org.springblade.system.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -185,4 +188,20 @@ public class Dept extends TenantEntity {
|
||||
@Schema(description = "是否平台公司:0否,1是")
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -43,6 +43,12 @@ public class MeasurementUnit extends BaseEntity {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 计量单位编码
|
||||
*/
|
||||
@Schema(description = "计量单位编码")
|
||||
private String unitCode;
|
||||
|
||||
/**
|
||||
* 计量单位
|
||||
*/
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* OA组织(公司/部门)分页同步结果
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "OA组织分页同步结果")
|
||||
public class OaOrgSyncPageVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "同步阶段:company / department")
|
||||
private String stage;
|
||||
|
||||
@Schema(description = "当前页")
|
||||
private Integer current;
|
||||
|
||||
@Schema(description = "每页条数")
|
||||
private Integer size;
|
||||
|
||||
@Schema(description = "OA总条数")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "本页从OA拉取的条数")
|
||||
private Integer fetchedCount;
|
||||
|
||||
@Schema(description = "本页同步成功条数")
|
||||
private Integer syncedCount;
|
||||
|
||||
@Schema(description = "本页跳过条数")
|
||||
private Integer skippedCount;
|
||||
|
||||
@Schema(description = "是否已到最后一页")
|
||||
private Boolean finished;
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* OA人员分页同步结果
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "OA人员分页同步结果")
|
||||
public class OaPersonSyncPageVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "当前页")
|
||||
private Integer current;
|
||||
|
||||
@Schema(description = "每页条数")
|
||||
private Integer size;
|
||||
|
||||
@Schema(description = "OA人员总条数")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "本页从OA拉取的条数")
|
||||
private Integer fetchedCount;
|
||||
|
||||
@Schema(description = "本页同步成功条数")
|
||||
private Integer syncedCount;
|
||||
|
||||
@Schema(description = "本页跳过条数")
|
||||
private Integer skippedCount;
|
||||
|
||||
@Schema(description = "是否已到最后一页")
|
||||
private Boolean finished;
|
||||
}
|
||||
+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
@@ -58,6 +58,9 @@ public class CustomerArchive extends TenantEntity {
|
||||
@Schema(description = "客商全称")
|
||||
private String fullName;
|
||||
|
||||
@Schema(description = "客商类型:external外部客商,internal内部组织")
|
||||
private String customerKind;
|
||||
|
||||
@Schema(description = "客商性质")
|
||||
private String customerNature;
|
||||
|
||||
|
||||
+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;
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Schema(description = "调度端司机搜索项")
|
||||
public class AdminDriverOptionVO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "司机ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "姓名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "绑定车牌")
|
||||
private String vehicleNo;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 调度端首页快捷入口角标
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "调度端首页角标")
|
||||
public class AdminHomeBadgesVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "异常处置待办数(disposalStatus≠completed)")
|
||||
private long exception;
|
||||
|
||||
@Schema(description = "风险记录待办数(disposalStatus=pending)")
|
||||
private long risk;
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 调度端首页顶部统计(对齐小程序 admin/home)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "调度端首页运单状态统计")
|
||||
public class AdminHomeStatsVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运输中(businessStatus=running)")
|
||||
private long transporting;
|
||||
|
||||
@Schema(description = "待接单(businessStatus=pending)")
|
||||
private long pendingAccept;
|
||||
|
||||
@Schema(description = "在途异常(异常处置状态≠已完成)")
|
||||
private long exception;
|
||||
|
||||
@Schema(description = "已完成(businessStatus=completed)")
|
||||
private long completed;
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 调度端首页聚合数据(统计 + 角标 + 待处理 feed + 用户名)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "调度端首页聚合")
|
||||
public class AdminHomeVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "当前登录用户姓名")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "顶部运单状态统计")
|
||||
private AdminHomeStatsVO stats = new AdminHomeStatsVO();
|
||||
|
||||
@Schema(description = "快捷入口角标")
|
||||
private AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||
|
||||
@Schema(description = "待处理事项(异常处置状态≠已完成)")
|
||||
private List<AdminTodoItemVO> feed = new ArrayList<>();
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 调度端首页「待处理事项」条目
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "调度端首页待处理事项")
|
||||
public class AdminTodoItemVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "条目ID(异常处置ID)")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "类型:exception=异常待处置 / reassign=重新派单待处理")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "相对时间文案,如「2分钟」")
|
||||
private String timeAgo;
|
||||
|
||||
@Schema(description = "摘要描述")
|
||||
private String desc;
|
||||
|
||||
@Schema(description = "运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Schema(description = "操作按钮文案")
|
||||
private String actionLabel;
|
||||
|
||||
@Schema(description = "跳转路径(小程序内路径)")
|
||||
private String targetUrl;
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Schema(description = "调度端车牌搜索项")
|
||||
public class AdminVehicleOptionVO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "车牌号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "关联司机名(可选)")
|
||||
private String driverName;
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 调度端运单列表卡片(对齐小程序 admin/waybill-list)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "调度端运单列表卡片")
|
||||
public class AdminWaybillCardVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运单ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Schema(description = "起点")
|
||||
private String fromName;
|
||||
|
||||
@Schema(description = "终点")
|
||||
private String toName;
|
||||
|
||||
@Schema(description = "货物名称")
|
||||
private String cargo;
|
||||
|
||||
@Schema(description = "重量(带单位)")
|
||||
private String weight;
|
||||
|
||||
@Schema(description = "计划开始时间")
|
||||
private String planTime;
|
||||
|
||||
@Schema(description = "计划结束时间")
|
||||
private String planTimeEnd;
|
||||
|
||||
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "承运方")
|
||||
private String carrierName;
|
||||
|
||||
@Schema(description = "司机姓名")
|
||||
private String driverName;
|
||||
|
||||
@Schema(description = "车牌号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "是否有未完成异常")
|
||||
private Boolean hasException;
|
||||
|
||||
@Schema(description = "运输组织类型:common普通 / load配载")
|
||||
private String transportType;
|
||||
|
||||
@Schema(description = "运输方式:road / 公路运输 / 铁路运输 等")
|
||||
private String transportMode;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private String createTime;
|
||||
|
||||
@Schema(description = "需重新派单(司机已拒单)")
|
||||
private Boolean needReassign;
|
||||
|
||||
@Schema(description = "采购方是否已付款(预留)")
|
||||
private Boolean buyerPaid;
|
||||
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 调度端运单详情(对齐小程序 pages/waybill/detail)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "调度端运单详情")
|
||||
public class AdminWaybillDetailVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运单ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "起点名称")
|
||||
private String fromName;
|
||||
|
||||
@Schema(description = "终点名称")
|
||||
private String toName;
|
||||
|
||||
@Schema(description = "起点地址")
|
||||
private String fromAddress;
|
||||
|
||||
@Schema(description = "终点地址")
|
||||
private String toAddress;
|
||||
|
||||
@Schema(description = "装货地址")
|
||||
private String pickupAddress;
|
||||
|
||||
@Schema(description = "卸货地址")
|
||||
private String unloadAddress;
|
||||
|
||||
@Schema(description = "货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "货物数量(带单位)")
|
||||
private String cargoQuantity;
|
||||
|
||||
@Schema(description = "重量(带单位)")
|
||||
private String weight;
|
||||
|
||||
@Schema(description = "合计货重")
|
||||
private String totalWeight;
|
||||
|
||||
@Schema(description = "运输方式文案:公路运输 / 铁路运输 等")
|
||||
private String transportType;
|
||||
|
||||
@Schema(description = "运输方式字典值:road 等")
|
||||
private String transportMode;
|
||||
|
||||
@Schema(description = "运输组织:common普通 / load配载")
|
||||
private String transportOrgType;
|
||||
|
||||
@Schema(description = "计划发货时间")
|
||||
private String planShipTime;
|
||||
|
||||
@Schema(description = "计划完成时间")
|
||||
private String planFinishTime;
|
||||
|
||||
@Schema(description = "承运方")
|
||||
private String carrierName;
|
||||
|
||||
@Schema(description = "司机姓名")
|
||||
private String driverName;
|
||||
|
||||
@Schema(description = "司机联系方式")
|
||||
private String driverPhone;
|
||||
|
||||
@Schema(description = "车牌号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "是否有未完成异常")
|
||||
private Boolean hasException;
|
||||
|
||||
@Schema(description = "最近一条未完成异常处置ID(有异常时返回)")
|
||||
private Long exceptionId;
|
||||
|
||||
@Schema(description = "需重新派单")
|
||||
private Boolean needReassign;
|
||||
|
||||
@Schema(description = "司机接单状态:pending/accepted/rejected")
|
||||
private String acceptStatus;
|
||||
|
||||
@Schema(description = "司机拒绝接单原因")
|
||||
private String rejectReason;
|
||||
|
||||
@Schema(description = "原指派司机ID")
|
||||
private Long driverId;
|
||||
|
||||
@Schema(description = "装卸点列表")
|
||||
private List<AdminRoutePointVO> routePoints;
|
||||
|
||||
@Schema(description = "过程打卡节点(与司机端 punchNodes 同结构)")
|
||||
private List<DriverPunchNodeVO> punchNodes;
|
||||
|
||||
@Schema(description = "途打卡记录")
|
||||
private List<DriverEnrouteRecordVO> enrouteRecords;
|
||||
|
||||
@Data
|
||||
@Schema(description = "装卸点")
|
||||
public static class AdminRoutePointVO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "点位名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "详细地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "状态:pending/done/active")
|
||||
private String status;
|
||||
}
|
||||
|
||||
}
|
||||
+4
-4
@@ -82,10 +82,6 @@ public class CustomerArchiveVO extends CustomerArchive {
|
||||
@Schema(description = "是否保存变更记录(仅提交时为 true,保存时不记录)")
|
||||
private Boolean recordChange;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "客商类型:external外部客商,internal内部组织")
|
||||
private String customerKind;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "资金使用风险等级:high、medium、none")
|
||||
private String fundUseRisk;
|
||||
@@ -98,6 +94,10 @@ public class CustomerArchiveVO extends CustomerArchive {
|
||||
@Schema(description = "资金使用率(百分比)")
|
||||
private BigDecimal fundUseRate;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "评分复评状态")
|
||||
private String reviewStatus;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "已付金额合计(元)")
|
||||
private BigDecimal usedFundLimit;
|
||||
|
||||
+51
@@ -133,4 +133,55 @@ public class DriverWaybillCardVO implements Serializable {
|
||||
@Schema(description = "过程配置中 punch=是 的打卡节点列表(详情返回)")
|
||||
private List<DriverPunchNodeVO> punchNodes;
|
||||
|
||||
/* ===== pages/waybill/detail 接单查看字段 ===== */
|
||||
|
||||
@Schema(description = "货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "装货地址")
|
||||
private String pickupAddress;
|
||||
|
||||
@Schema(description = "卸货地址")
|
||||
private String unloadAddress;
|
||||
|
||||
@Schema(description = "货物数量(带单位)")
|
||||
private String cargoQuantity;
|
||||
|
||||
@Schema(description = "运输方式文案:公路运输等")
|
||||
private String transportType;
|
||||
|
||||
@Schema(description = "计划发货时间")
|
||||
private String planShipTime;
|
||||
|
||||
@Schema(description = "计划完成时间")
|
||||
private String planFinishTime;
|
||||
|
||||
@Schema(description = "合计货重")
|
||||
private String totalWeight;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "装卸点(详情返回)")
|
||||
private List<DriverRoutePointVO> routePoints;
|
||||
|
||||
@Schema(description = "过程配置 JSON(详情返回,供前端兜底推导打卡节点)")
|
||||
private String processJson;
|
||||
|
||||
@Data
|
||||
@Schema(description = "司机端装卸点")
|
||||
public static class DriverRoutePointVO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "点位名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "详细地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "状态:pending/done/active")
|
||||
private String status;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -46,6 +46,10 @@ public class LoadingManageVO extends LoadingManage {
|
||||
@Schema(description = "业务状态名称")
|
||||
private String businessStatusName;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "是否存在司机拒绝接单(可重新派单)")
|
||||
private Boolean driverRejected;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "运单号")
|
||||
private String waybillNo;
|
||||
|
||||
+14
@@ -30,6 +30,8 @@ import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 项目立项视图实体类
|
||||
@@ -104,4 +106,16 @@ public class ProjectApplyVO extends ProjectApply {
|
||||
@Schema(description = "风险计算额度基数")
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
+22
@@ -27,8 +27,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.TransportPlan;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -72,5 +75,24 @@ public class TransportPlanVO extends TransportPlan {
|
||||
@Schema(description = "计划调度生成的运单")
|
||||
private List<WaybillVO> dispatchRows;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "计划开始日期起")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate planStartDateStart;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "计划开始日期止")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate planStartDateEnd;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "创建开始时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTimeStart;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "创建结束时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTimeEnd;
|
||||
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 运单车辆实时定位结果
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运单车辆实时定位结果")
|
||||
public class WaybillLocateVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运单ID")
|
||||
private Long waybillId;
|
||||
|
||||
@Schema(description = "车牌号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "经度")
|
||||
private BigDecimal longitude;
|
||||
|
||||
@Schema(description = "纬度")
|
||||
private BigDecimal latitude;
|
||||
|
||||
@Schema(description = "地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "定位时间")
|
||||
private String locateTime;
|
||||
|
||||
@Schema(description = "速度")
|
||||
private String speed;
|
||||
|
||||
@Schema(description = "方向")
|
||||
private String direction;
|
||||
|
||||
@Schema(description = "LBS原始数据")
|
||||
private Map<String, Object> rawData;
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 运单历史轨迹结果
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运单历史轨迹结果")
|
||||
public class WaybillTrackVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运单ID")
|
||||
private Long waybillId;
|
||||
|
||||
@Schema(description = "车牌号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "开始日期")
|
||||
private String startDate;
|
||||
|
||||
@Schema(description = "结束日期")
|
||||
private String endDate;
|
||||
|
||||
@Schema(description = "轨迹点数量")
|
||||
private Integer total;
|
||||
|
||||
@Schema(description = "轨迹点列表")
|
||||
private List<WaybillTrackPointVO> points = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Schema(description = "轨迹点")
|
||||
public static class WaybillTrackPointVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "车牌号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "经度")
|
||||
private BigDecimal longitude;
|
||||
|
||||
@Schema(description = "纬度")
|
||||
private BigDecimal latitude;
|
||||
|
||||
@Schema(description = "定位时间")
|
||||
private String locateTime;
|
||||
|
||||
@Schema(description = "速度")
|
||||
private String speed;
|
||||
|
||||
@Schema(description = "方向")
|
||||
private String direction;
|
||||
|
||||
@Schema(description = "地址")
|
||||
private String address;
|
||||
}
|
||||
}
|
||||
+15
@@ -61,6 +61,7 @@ public interface IUserClient {
|
||||
String SAVE_IAM_USER = API_PREFIX + "/save-iam-user";
|
||||
String REGISTER_USER = API_PREFIX + "/register-user";
|
||||
String REMOVE_USER = API_PREFIX + "/remove-user";
|
||||
String BIND_WX_MINI_OPENID = API_PREFIX + "/bind-wx-mini-openid";
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
@@ -187,4 +188,18 @@ public interface IUserClient {
|
||||
@PostMapping(REMOVE_USER)
|
||||
R<Boolean> removeUser(@RequestParam("tenantIds") String tenantIds);
|
||||
|
||||
/**
|
||||
* 绑定微信小程序 openid(写入 blade_user_oauth,source=WECHAT_MINI)
|
||||
*
|
||||
* @param tenantId 租户ID
|
||||
* @param userId 用户ID
|
||||
* @param openid 微信 openid
|
||||
* @param phone 手机号(可选,写入 username)
|
||||
*/
|
||||
@PostMapping(BIND_WX_MINI_OPENID)
|
||||
R<Boolean> bindWxMiniOpenId(@RequestParam("tenantId") String tenantId,
|
||||
@RequestParam("userId") Long userId,
|
||||
@RequestParam("openid") String openid,
|
||||
@RequestParam(value = "phone", required = false) String phone);
|
||||
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 更换手机号参数
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "更换手机号参数")
|
||||
public class PhoneChangeDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "新手机号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String newPhone;
|
||||
|
||||
@Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "新手机号短信验证码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String code;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 原手机号短信校验参数
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "原手机号短信校验参数")
|
||||
public class PhoneVerifyDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String code;
|
||||
}
|
||||
+4
@@ -44,6 +44,10 @@ public class OpenApiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
BladeApplication.disableNacosLaunchConfig();
|
||||
// 当前处理人刷新依赖 RedisLockClient;Nacos 全局 blade.lock.enabled=false 时仍需为本服务开启
|
||||
if (System.getProperty("blade.lock.enabled") == null) {
|
||||
System.setProperty("blade.lock.enabled", "true");
|
||||
}
|
||||
BladeApplication.run(AppConstant.APPLICATION_OPENAPI_NAME, OpenApiApplication.class, args);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.Hidden;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.openapi.mk.api.IApi4MK;
|
||||
import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO;
|
||||
@@ -49,6 +51,7 @@ public class Api4MK implements IApi4MK {
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
public FR<Boolean> processCommonCallback(Api4MKProcessApprovalDTO param) {
|
||||
log.info("mk流程通用回调 操作名称:{} 参数:{}", ProcessOperationType.getOperationName(param.getOperation()), JSON.toJSONString(param));
|
||||
callback(param, ProcessHandler::approve);
|
||||
|
||||
@@ -10,6 +10,7 @@ spring:
|
||||
- nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true
|
||||
- nacos:blade-openapi-dynamictp.yaml?group=DEFAULT_GROUP&refreshEnabled=true
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true
|
||||
- optional:classpath:openapi-lock.yaml
|
||||
cloud:
|
||||
nacos:
|
||||
username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# openapi 当前处理人刷新依赖 Redisson 分布式锁。
|
||||
# 该文件必须在 nacos blade-*.yaml 之后导入,用于覆盖全局 blade.lock.enabled=false。
|
||||
# 连接信息与业务 Redis 保持一致,避免 Redisson 因缺少密码出现 NOAUTH。
|
||||
blade:
|
||||
lock:
|
||||
enabled: true
|
||||
address: redis://${spring.data.redis.host:127.0.0.1}:${spring.data.redis.port:6379}
|
||||
password: ${spring.data.redis.password:}
|
||||
database: ${spring.data.redis.database:0}
|
||||
@@ -45,6 +45,10 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-user-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-resource-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-process-api</artifactId>
|
||||
@@ -58,6 +62,10 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-mk-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-transport-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 其他依赖 -->
|
||||
<dependency>
|
||||
|
||||
+18
-4
@@ -17,6 +17,7 @@ import org.springblade.process.pojo.dto.ApprovalDTO;
|
||||
import org.springblade.process.pojo.vo.ApprovalVO;
|
||||
import org.springblade.process.pojo.vo.ProcessApprovedRecordVO;
|
||||
import org.springblade.process.service.IBusinessProcessService;
|
||||
import org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -52,29 +53,42 @@ public class BusinessProcessController extends BladeController {
|
||||
return R.data(pages);
|
||||
}
|
||||
|
||||
@GetMapping("/isEditView")
|
||||
@PostMapping("/processSubmit")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "提交MK审核流", description = "调用mk processSubmit,传入templateCode/submitIdentity/formInstanceId")
|
||||
public R<String> processSubmit(@Validated @RequestBody MKProcessCreateDTO 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")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "是否编辑页", description = "传入业务id")
|
||||
public R<Boolean> isEditView(@Valid @NotBlank(message = "业务id不能为空") String bizId) {
|
||||
return R.data(businessProcessService.isEditView(bizId));
|
||||
}
|
||||
|
||||
@GetMapping("/getMKApprovalUrl")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "获取mk审批页链接", description = "传入业务id或流程实例id")
|
||||
public R<String> getMKApprovalUrl(String bizId, String processInstanceId) {
|
||||
return R.data(businessProcessService.getMKApprovalUrl(bizId, processInstanceId));
|
||||
}
|
||||
|
||||
@GetMapping("/getApprovedRecords")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "查询审批记录", description = "传入业务id或流程实例id")
|
||||
public R<List<ProcessApprovedRecordVO>> getApprovedRecords(String bizId, String processInstanceId) {
|
||||
return R.data(businessProcessService.queryApprovedRecords(bizId, processInstanceId));
|
||||
}
|
||||
|
||||
@GetMapping("/downloadFile")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "下载附件", description = "传入附件id")
|
||||
public void downloadFile(HttpServletResponse response, @Valid @NotBlank(message = "附件id不能为空") String fileId) {
|
||||
businessProcessService.downloadFile(response, fileId);
|
||||
|
||||
+20
@@ -3,6 +3,8 @@ package org.springblade.process.feign;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.process.pojo.dto.BusinessProcessCurrentHandlerRefreshDTO;
|
||||
import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO;
|
||||
@@ -13,8 +15,10 @@ import org.springblade.process.pojo.vo.ProcessApprovedRecordVO;
|
||||
import org.springblade.process.pojo.vo.ProcessTodoVO;
|
||||
import org.springblade.process.service.IBusinessProcessService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
@@ -74,4 +78,20 @@ public class BusinessProcessClient implements IBusinessProcessClient {
|
||||
public FR<List<ProcessApprovedRecordVO>> queryApprovedRecordsNoAttachments(String bizId, String processInstanceId) {
|
||||
return FR.data(businessProcessService.queryApprovedRecordsNoAttachments(bizId, processInstanceId));
|
||||
}
|
||||
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@GetMapping(GET_CURRENT_NODES)
|
||||
@Override
|
||||
public FR<Object> getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId,
|
||||
@RequestParam(value = "loginName", required = false) String 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));
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springblade.process.pojo.dto.*;
|
||||
import org.springblade.process.pojo.entity.BusinessProcess;
|
||||
import org.springblade.process.pojo.vo.*;
|
||||
import org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,6 +26,40 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
|
||||
*/
|
||||
BusinessProcessVO submitBusinessProcess(BusinessProcessSubmitDTO<?> param);
|
||||
|
||||
/**
|
||||
* 直接调用 MK processSubmit 提交流程
|
||||
*
|
||||
* @param param MK 流程创建参数
|
||||
* @return 流程实例 id
|
||||
*/
|
||||
String processSubmit(MKProcessCreateDTO param);
|
||||
|
||||
/**
|
||||
* 按业务表单实例 id 删除 MK 流程(不删除本地业务流程记录,供驳回后重新提交使用)
|
||||
*
|
||||
* @param formInstanceId 业务表单实例 id
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean processDelete(String formInstanceId);
|
||||
|
||||
/**
|
||||
* 获取流程当前节点详情
|
||||
*
|
||||
* @param processInstanceId 流程实例id
|
||||
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
|
||||
* @return 当前节点列表
|
||||
*/
|
||||
List<?> getCurrentNodes(String processInstanceId, String loginName);
|
||||
|
||||
/**
|
||||
* 获取流程实例详情
|
||||
*
|
||||
* @param processInstanceId 流程实例id
|
||||
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
|
||||
* @return 流程实例详情
|
||||
*/
|
||||
Object getProcessInfo(String processInstanceId, String loginName);
|
||||
|
||||
/**
|
||||
* 修改业务流程状态
|
||||
* @param param
|
||||
|
||||
+264
@@ -15,6 +15,7 @@ import org.springblade.process.pojo.enums.ApproveStatusEnum;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.log.utils.AssertUtils;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.process.convert.ApprovalConvert;
|
||||
import org.springblade.process.convert.BusinessProcessConvert;
|
||||
@@ -24,6 +25,8 @@ import org.springblade.process.pojo.entity.BusinessProcess;
|
||||
import org.springblade.process.pojo.enums.TodoStatus;
|
||||
import org.springblade.process.pojo.vo.*;
|
||||
import org.springblade.process.service.IBusinessProcessService;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.system.service.IUserService;
|
||||
import org.springblade.thirdparty.mk.config.MKProperties;
|
||||
import org.springblade.thirdparty.mk.constant.MKConstant;
|
||||
import org.springblade.thirdparty.mk.constant.MKDoc;
|
||||
@@ -39,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.vo.*;
|
||||
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.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 业务流程关联表 服务实现类
|
||||
@@ -61,6 +69,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
private final IMKService mkService;
|
||||
private final MKProperties mkProperties;
|
||||
private final ApprovalConvert approvalConvert;
|
||||
private final IUserService userService;
|
||||
private final ICustomerArchiveClient customerArchiveClient;
|
||||
private final IMkProcessClient mkProcessClient;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
@@ -114,6 +125,259 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
return businessProcessVO;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public String processSubmit(MKProcessCreateDTO param) {
|
||||
AssertUtils.notNull(param, "提交流程参数不能为空");
|
||||
AssertUtils.notBlank(param.getFormInstanceId(), "表单实例id不能为空");
|
||||
AssertUtils.notBlank(param.getTemplateCode(), "模板编码不能为空");
|
||||
// 前端拿到的手机号可能经 @Sensitive 脱敏(如 137****8880),这里从库取真实手机号覆盖
|
||||
String loginName = resolveCurrentUserPhone();
|
||||
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
|
||||
param.setSubmitIdentity(loginName);
|
||||
param.setLoginName(loginName);
|
||||
String bizType = param.getBizType();
|
||||
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");
|
||||
|
||||
Long bizId;
|
||||
try {
|
||||
bizId = Long.valueOf(param.getFormInstanceId());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ServiceException("表单实例id格式不正确");
|
||||
}
|
||||
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
|
||||
.eq(BusinessProcess::getBizId, bizId)
|
||||
);
|
||||
if (businessProcess == null) {
|
||||
businessProcess = new BusinessProcess();
|
||||
businessProcess.setBizId(bizId);
|
||||
businessProcess.setProcessType(param.getTemplateCode());
|
||||
businessProcess.setSubject(param.getSubject());
|
||||
businessProcess.setPromoterId(AuthUtil.getUserId());
|
||||
businessProcess.setPromoterName(AuthUtil.getNickName());
|
||||
businessProcess.setPromoterLoginName(loginName);
|
||||
businessProcess.setSubmitTime(new Date());
|
||||
} else {
|
||||
businessProcess.setProcessType(param.getTemplateCode());
|
||||
if (StringUtil.isNotBlank(param.getSubject())) {
|
||||
businessProcess.setSubject(param.getSubject());
|
||||
}
|
||||
businessProcess.setPromoterLoginName(loginName);
|
||||
if (businessProcess.getSubmitTime() == null) {
|
||||
businessProcess.setSubmitTime(new Date());
|
||||
}
|
||||
}
|
||||
businessProcess.setProcessInstanceId(processInstanceId);
|
||||
businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue());
|
||||
this.saveOrUpdate(businessProcess);
|
||||
this.getCurrentNodes(processInstanceId, loginName);
|
||||
Object processInfo = this.getProcessInfo(processInstanceId, loginName);
|
||||
this.syncBizFromProcessInfo(bizType, param.getFormInstanceId(), processInfo);
|
||||
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
|
||||
public List<?> getCurrentNodes(String processInstanceId, String loginName) {
|
||||
if (StringUtils.isBlank(processInstanceId)) {
|
||||
log.warn("查询当前节点失败,processInstanceId为空");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String resolvedLoginName = loginName;
|
||||
if (StringUtils.isBlank(resolvedLoginName)) {
|
||||
BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId);
|
||||
resolvedLoginName = resolvePromoterLoginName(businessProcess, null);
|
||||
}
|
||||
if (StringUtils.isBlank(resolvedLoginName)) {
|
||||
log.warn("查询当前节点失败,loginName为空 processInstanceId={}", processInstanceId);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
List<MKNodeVO> currentNodes = mkService.getCurrentNodes(processInstanceId, resolvedLoginName);
|
||||
log.info("获取流程当前节点详情 processInstanceId={} loginName={} result={}",
|
||||
processInstanceId, resolvedLoginName, JSON.toJSONString(currentNodes));
|
||||
return currentNodes == null ? Collections.emptyList() : currentNodes;
|
||||
} catch (Exception e) {
|
||||
log.error("查询当前节点异常 processInstanceId={} loginName={}", processInstanceId, resolvedLoginName, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前登录用户实体读取真实手机号(绕过接口返回脱敏)
|
||||
*/
|
||||
private String resolveCurrentUserPhone() {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (userId != null) {
|
||||
User user = userService.getById(userId);
|
||||
if (user != null && StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) {
|
||||
return user.getPhone().trim();
|
||||
}
|
||||
if (user != null && StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
|
||||
return user.getAccount().trim();
|
||||
}
|
||||
}
|
||||
String account = AuthUtil.getUserAccount();
|
||||
if (StringUtil.isNotBlank(account) && account.matches("^1\\d{10}$")) {
|
||||
return account.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public String updateBusinessProcessStatus(BusinessProcessUpdateDTO param) {
|
||||
|
||||
+3
-1
@@ -28,13 +28,16 @@ package org.springblade.system;
|
||||
import org.springblade.core.cloud.client.BladeCloudApplication;
|
||||
import org.springblade.core.launch.BladeApplication;
|
||||
import org.springblade.core.launch.constant.AppConstant;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springblade.system.props.IamSyncProperties;
|
||||
|
||||
/**
|
||||
* 系统模块启动器
|
||||
* @author Chill
|
||||
*/
|
||||
@ComponentScan(basePackages = {"org.springblade.system", "org.springblade.process"})
|
||||
@EnableConfigurationProperties(IamSyncProperties.class)
|
||||
@BladeCloudApplication
|
||||
public class SystemApplication {
|
||||
|
||||
@@ -44,4 +47,3 @@ public class SystemApplication {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+58
-4
@@ -50,8 +50,10 @@ import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.system.pojo.enums.DictEnum;
|
||||
import org.springblade.system.pojo.vo.DeptVO;
|
||||
import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
|
||||
import org.springblade.system.pojo.vo.UserVO;
|
||||
import org.springblade.system.service.IDeptService;
|
||||
import org.springblade.system.service.IOASyncService;
|
||||
import org.springblade.system.wrapper.DeptWrapper;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -73,6 +75,7 @@ import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
|
||||
public class DeptController extends BladeController {
|
||||
|
||||
private final IDeptService deptService;
|
||||
private final IOASyncService oaSyncService;
|
||||
|
||||
/**
|
||||
* 详情
|
||||
@@ -159,12 +162,63 @@ public class DeptController extends BladeController {
|
||||
return R.fail("操作失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步IAM组织。
|
||||
*/
|
||||
@IsAdmin
|
||||
@PostMapping("/sync-iam-organizations")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "同步IAM组织")
|
||||
public R<Integer> 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按页同步公司
|
||||
*/
|
||||
@IsAdmin
|
||||
@PostMapping("/sync-oa-company")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "同步OA公司")
|
||||
public R<OaOrgSyncPageVO> syncOaCompany(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size) {
|
||||
return R.data(oaSyncService.syncCompanyPage(current, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OA按页同步部门(需先完成公司同步)
|
||||
*/
|
||||
@IsAdmin
|
||||
@PostMapping("/sync-oa-department")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "同步OA部门")
|
||||
public R<OaOrgSyncPageVO> syncOaDepartment(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size) {
|
||||
return R.data(oaSyncService.syncDepartmentPage(current, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
@IsAdmin
|
||||
@PostMapping("/remove")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "删除", description = "传入ids")
|
||||
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
@@ -177,7 +231,7 @@ public class DeptController extends BladeController {
|
||||
*/
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@GetMapping("/select")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "下拉数据源", description = "传入id集合")
|
||||
public R<List<Dept>> select(Long userId, String deptId) {
|
||||
if (Func.isNotEmpty(userId)) {
|
||||
@@ -194,7 +248,7 @@ public class DeptController extends BladeController {
|
||||
*/
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@GetMapping("/platform-company-select")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表")
|
||||
public R<List<Dept>> platformCompanySelect() {
|
||||
return R.data(deptService.listPlatformCompany());
|
||||
@@ -205,7 +259,7 @@ public class DeptController extends BladeController {
|
||||
*/
|
||||
@IsAdmin
|
||||
@GetMapping("/dept-leader-info")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "获取部门的主管信息", description = "传入deptId")
|
||||
public R<List<UserVO>> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) {
|
||||
List<UserVO> list = deptService.deptLeaderInfo(deptId);
|
||||
|
||||
+276
-263
@@ -1,263 +1,276 @@
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.excel.util.ExcelUtil;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tenant.annotation.NonDS;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.excel.PortTerminalExcel;
|
||||
import org.springblade.system.excel.PortTerminalExportExcel;
|
||||
import org.springblade.system.excel.PortTerminalImporter;
|
||||
import org.springblade.system.pojo.entity.PortTerminal;
|
||||
import org.springblade.system.pojo.vo.PortTerminalVO;
|
||||
import org.springblade.system.service.IPortTerminalService;
|
||||
import org.springblade.system.wrapper.PortTerminalWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 港口码头主数据 控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@NonDS
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "port_terminal")
|
||||
@RequestMapping("/port-terminal")
|
||||
@Tag(name = "港口码头主数据", description = "港口码头主数据")
|
||||
public class PortTerminalController extends BladeController {
|
||||
|
||||
private static final int DEFAULT_CURRENT = 1;
|
||||
private static final int DEFAULT_SIZE = 10;
|
||||
private static final int MAX_SIZE = 100;
|
||||
private static final String SOURCE_INITIAL = "初始化录入";
|
||||
private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
|
||||
private static final String SOURCE_INITIAL_OLD = "初始导入";
|
||||
private static final String SOURCE_MANUAL = "手动录入";
|
||||
private static final String SOURCE_MANUAL_OLD = "手工导入";
|
||||
|
||||
private final IPortTerminalService portTerminalService;
|
||||
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "详情", description = "传入portTerminal")
|
||||
public R<PortTerminalVO> detail(PortTerminal portTerminal) {
|
||||
if (Func.isEmpty(portTerminal.getId())) {
|
||||
return R.fail("主键不能为空");
|
||||
}
|
||||
PortTerminal detail = portTerminalService.getById(portTerminal.getId());
|
||||
if (Func.isEmpty(detail)) {
|
||||
return R.fail("港口码头不存在");
|
||||
}
|
||||
detail.setDataSource(normalizeDataSource(detail.getDataSource()));
|
||||
return R.data(PortTerminalWrapper.build().entityVO(detail));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "分页", description = "传入portTerminal")
|
||||
public R<IPage<PortTerminalVO>> list(PortTerminalVO portTerminal, Query query) {
|
||||
IPage<PortTerminalVO> pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal);
|
||||
return R.data(pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或修改
|
||||
*/
|
||||
@PostMapping("/submit")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "新增或修改", description = "传入portTerminal")
|
||||
public R submit(@Valid @RequestBody PortTerminal portTerminal) {
|
||||
return R.status(portTerminalService.submit(portTerminal));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
@PostMapping("/remove")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "逻辑删除", description = "传入ids")
|
||||
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
|
||||
if (Func.isEmpty(ids)) {
|
||||
return R.fail("主键不能为空");
|
||||
}
|
||||
return R.status(portTerminalService.deleteLogic(Func.toLongList(ids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或停用
|
||||
*/
|
||||
@PostMapping("/status")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "启用或停用", description = "传入id和status")
|
||||
public R status(@Parameter(description = "主键", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "状态", required = true) @RequestParam Integer status) {
|
||||
return R.status(portTerminalService.changeStatus(id, status));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上级港口下拉数据源
|
||||
*/
|
||||
@GetMapping("/port-select")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "上级港口下拉数据源")
|
||||
public R<List<PortTerminal>> portSelect() {
|
||||
List<PortTerminal> ports = portTerminalService.selectEnabledPorts();
|
||||
ports.forEach(port -> port.setDataSource(normalizeDataSource(port.getDataSource())));
|
||||
return R.data(ports);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入港口码头主数据
|
||||
*/
|
||||
@PostMapping("/import-port-terminal")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "导入港口码头主数据", description = "传入excel")
|
||||
public R importPortTerminal(MultipartFile file, HttpServletResponse response) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return R.fail("上传文件不能为空");
|
||||
}
|
||||
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
|
||||
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||
return R.fail("请上传 .xls,.xlsx 标准格式文件");
|
||||
}
|
||||
List<PortTerminalExcel> failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
|
||||
return null;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出港口码头主数据
|
||||
*/
|
||||
@GetMapping("/export-port-terminal")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "导出港口码头主数据")
|
||||
public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map<String, Object> portTerminal, HttpServletResponse response) {
|
||||
Object ids = portTerminal.remove("ids");
|
||||
Object dataSource = portTerminal.remove("dataSource");
|
||||
portTerminal.remove("Blade-Auth");
|
||||
portTerminal.remove("Authorization");
|
||||
portTerminal.remove("access_token");
|
||||
normalizeRegionCodeCondition(portTerminal);
|
||||
QueryWrapper<PortTerminal> queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
|
||||
applyDataSourceCondition(queryWrapper, dataSource);
|
||||
if (Func.isNotEmpty(ids)) {
|
||||
queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
|
||||
}
|
||||
List<PortTerminalExportExcel> list = portTerminalService.exportPortTerminal(queryWrapper);
|
||||
ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出模板
|
||||
*/
|
||||
@GetMapping("/export-template")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "导出模板")
|
||||
public void exportTemplate(HttpServletResponse response) {
|
||||
List<PortTerminalExcel> list = new ArrayList<>();
|
||||
ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class);
|
||||
}
|
||||
|
||||
private Query normalizeQuery(Query query) {
|
||||
if (query == null) {
|
||||
query = new Query();
|
||||
}
|
||||
if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
|
||||
query.setCurrent(DEFAULT_CURRENT);
|
||||
}
|
||||
if (query.getSize() == null || query.getSize() <= 0) {
|
||||
query.setSize(DEFAULT_SIZE);
|
||||
}
|
||||
if (query.getSize() > MAX_SIZE) {
|
||||
query.setSize(MAX_SIZE);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private void normalizeRegionCodeCondition(Map<String, Object> params) {
|
||||
Object regionCode = params.remove("regionCode");
|
||||
if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
|
||||
params.put("districtCode", regionCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDataSourceCondition(QueryWrapper<PortTerminal> queryWrapper, Object dataSource) {
|
||||
String value = Func.toStrWithEmpty(dataSource, "");
|
||||
if (Func.isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
if (SOURCE_INITIAL.equals(value)) {
|
||||
queryWrapper.in("data_source", SOURCE_INITIAL, SOURCE_INITIAL_IMPORT, SOURCE_INITIAL_OLD);
|
||||
} else if (SOURCE_MANUAL.equals(value)) {
|
||||
queryWrapper.in("data_source", SOURCE_MANUAL, SOURCE_MANUAL_OLD);
|
||||
} else {
|
||||
queryWrapper.eq("data_source", value);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDataSource(String dataSource) {
|
||||
if (SOURCE_INITIAL_IMPORT.equals(dataSource) || SOURCE_INITIAL_OLD.equals(dataSource)) {
|
||||
return SOURCE_INITIAL;
|
||||
}
|
||||
return SOURCE_MANUAL_OLD.equals(dataSource) ? SOURCE_MANUAL : dataSource;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.excel.util.ExcelUtil;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tenant.annotation.NonDS;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
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.PortTerminalExportExcel;
|
||||
import org.springblade.system.excel.PortTerminalImporter;
|
||||
import org.springblade.system.pojo.entity.PortTerminal;
|
||||
import org.springblade.system.pojo.vo.PortTerminalVO;
|
||||
import org.springblade.system.service.IPortTerminalService;
|
||||
import org.springblade.system.wrapper.PortTerminalWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 港口码头主数据 控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@NonDS
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "port_terminal")
|
||||
@RequestMapping("/port-terminal")
|
||||
@Tag(name = "港口码头主数据", description = "港口码头主数据")
|
||||
public class PortTerminalController extends BladeController {
|
||||
|
||||
private static final int DEFAULT_CURRENT = 1;
|
||||
private static final int DEFAULT_SIZE = 10;
|
||||
private static final int MAX_SIZE = 100;
|
||||
private static final String SOURCE_INITIAL = "初始化录入";
|
||||
private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
|
||||
private static final String SOURCE_INITIAL_OLD = "初始导入";
|
||||
private static final String SOURCE_MANUAL = "手动录入";
|
||||
private static final String SOURCE_MANUAL_OLD = "手工导入";
|
||||
|
||||
private final IPortTerminalService portTerminalService;
|
||||
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "详情", description = "传入portTerminal")
|
||||
public R<PortTerminalVO> detail(PortTerminal portTerminal) {
|
||||
if (Func.isEmpty(portTerminal.getId())) {
|
||||
return R.fail("主键不能为空");
|
||||
}
|
||||
PortTerminal detail = portTerminalService.getById(portTerminal.getId());
|
||||
if (Func.isEmpty(detail)) {
|
||||
return R.fail("港口码头不存在");
|
||||
}
|
||||
detail.setDataSource(normalizeDataSource(detail.getDataSource()));
|
||||
return R.data(PortTerminalWrapper.build().entityVO(detail));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "分页", description = "传入portTerminal")
|
||||
public R<IPage<PortTerminalVO>> list(PortTerminalVO portTerminal, Query query) {
|
||||
IPage<PortTerminalVO> pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal);
|
||||
return R.data(pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或修改
|
||||
*/
|
||||
@PostMapping("/submit")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "新增或修改", description = "传入portTerminal")
|
||||
public R submit(@Valid @RequestBody PortTerminal portTerminal) {
|
||||
return R.status(portTerminalService.submit(portTerminal));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
@PostMapping("/remove")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "逻辑删除", description = "传入ids")
|
||||
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
|
||||
if (Func.isEmpty(ids)) {
|
||||
return R.fail("主键不能为空");
|
||||
}
|
||||
return R.status(portTerminalService.deleteLogic(Func.toLongList(ids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或停用
|
||||
*/
|
||||
@PostMapping("/status")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "启用或停用", description = "传入id和status")
|
||||
public R status(@Parameter(description = "主键", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "状态", required = true) @RequestParam Integer status) {
|
||||
return R.status(portTerminalService.changeStatus(id, status));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上级港口下拉数据源
|
||||
*/
|
||||
@GetMapping("/port-select")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "上级港口下拉数据源")
|
||||
public R<List<PortTerminal>> portSelect() {
|
||||
List<PortTerminal> ports = portTerminalService.selectEnabledPorts();
|
||||
ports.forEach(port -> port.setDataSource(normalizeDataSource(port.getDataSource())));
|
||||
return R.data(ports);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入港口码头主数据
|
||||
*/
|
||||
@PostMapping("/import-port-terminal")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "导入港口码头主数据", description = "传入excel")
|
||||
public R importPortTerminal(MultipartFile file, HttpServletResponse response) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return R.fail("上传文件不能为空");
|
||||
}
|
||||
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
|
||||
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||
return R.fail("请上传 .xls,.xlsx 标准格式文件");
|
||||
}
|
||||
try {
|
||||
portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
|
||||
} catch (ImportFailureException exception) {
|
||||
// 全失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
|
||||
exportFailure(response, exception.getFailureList());
|
||||
return null;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
|
||||
* <p>
|
||||
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
|
||||
*/
|
||||
private void exportFailure(HttpServletResponse response, List<?> failureList) {
|
||||
ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出港口码头主数据
|
||||
*/
|
||||
@GetMapping("/export-port-terminal")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "导出港口码头主数据")
|
||||
public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map<String, Object> portTerminal, HttpServletResponse response) {
|
||||
Object ids = portTerminal.remove("ids");
|
||||
Object dataSource = portTerminal.remove("dataSource");
|
||||
portTerminal.remove("Blade-Auth");
|
||||
portTerminal.remove("Authorization");
|
||||
portTerminal.remove("access_token");
|
||||
normalizeRegionCodeCondition(portTerminal);
|
||||
QueryWrapper<PortTerminal> queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
|
||||
applyDataSourceCondition(queryWrapper, dataSource);
|
||||
if (Func.isNotEmpty(ids)) {
|
||||
queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
|
||||
}
|
||||
List<PortTerminalExportExcel> list = portTerminalService.exportPortTerminal(queryWrapper);
|
||||
ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出模板
|
||||
*/
|
||||
@GetMapping("/export-template")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "导出模板")
|
||||
public void exportTemplate(HttpServletResponse response) {
|
||||
List<PortTerminalExcel> list = new ArrayList<>();
|
||||
ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class);
|
||||
}
|
||||
|
||||
private Query normalizeQuery(Query query) {
|
||||
if (query == null) {
|
||||
query = new Query();
|
||||
}
|
||||
if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
|
||||
query.setCurrent(DEFAULT_CURRENT);
|
||||
}
|
||||
if (query.getSize() == null || query.getSize() <= 0) {
|
||||
query.setSize(DEFAULT_SIZE);
|
||||
}
|
||||
if (query.getSize() > MAX_SIZE) {
|
||||
query.setSize(MAX_SIZE);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private void normalizeRegionCodeCondition(Map<String, Object> params) {
|
||||
Object regionCode = params.remove("regionCode");
|
||||
if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
|
||||
params.put("districtCode", regionCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDataSourceCondition(QueryWrapper<PortTerminal> queryWrapper, Object dataSource) {
|
||||
String value = Func.toStrWithEmpty(dataSource, "");
|
||||
if (Func.isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
if (SOURCE_INITIAL.equals(value)) {
|
||||
queryWrapper.in("data_source", SOURCE_INITIAL, SOURCE_INITIAL_IMPORT, SOURCE_INITIAL_OLD);
|
||||
} else if (SOURCE_MANUAL.equals(value)) {
|
||||
queryWrapper.in("data_source", SOURCE_MANUAL, SOURCE_MANUAL_OLD);
|
||||
} else {
|
||||
queryWrapper.eq("data_source", value);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDataSource(String dataSource) {
|
||||
if (SOURCE_INITIAL_IMPORT.equals(dataSource) || SOURCE_INITIAL_OLD.equals(dataSource)) {
|
||||
return SOURCE_INITIAL;
|
||||
}
|
||||
return SOURCE_MANUAL_OLD.equals(dataSource) ? SOURCE_MANUAL : dataSource;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+30
@@ -54,7 +54,9 @@ import org.springblade.core.tool.utils.StringPool;
|
||||
import org.springblade.system.excel.UserExcel;
|
||||
import org.springblade.system.excel.UserImporter;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
|
||||
import org.springblade.system.pojo.vo.UserVO;
|
||||
import org.springblade.system.service.IOASyncService;
|
||||
import org.springblade.system.service.IUserService;
|
||||
import org.springblade.system.wrapper.UserWrapper;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -77,6 +79,7 @@ import java.util.Map;
|
||||
public class UserController {
|
||||
|
||||
private final IUserService userService;
|
||||
private final IOASyncService oaSyncService;
|
||||
|
||||
/**
|
||||
* 查询单条
|
||||
@@ -157,6 +160,19 @@ public class UserController {
|
||||
return R.status(userService.submit(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OA按页同步人员,并按公司/部门生成组织后绑定到三级部门。
|
||||
*/
|
||||
@IsAdmin
|
||||
@PostMapping("/sync-iam-accounts")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "同步OA人员")
|
||||
public R<OaPersonSyncPageVO> syncIamAccounts(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size) {
|
||||
return R.data(oaSyncService.syncPersonFromUserList(current, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
@@ -216,6 +232,20 @@ public class UserController {
|
||||
return R.status(temp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户设置/重置登录密码(小程序首次设密、短信验证后改密)
|
||||
* <p>
|
||||
* 对外路径:/blade-system/user/password ;网关别名 /blade-user/password 亦可到达。
|
||||
*/
|
||||
@PostMapping("/password")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "设置登录密码", description = "当前登录用户设置密码,无需原密码")
|
||||
public R password(BladeUser user,
|
||||
@Parameter(description = "新密码", required = true) @RequestParam String password,
|
||||
@Parameter(description = "确认密码", required = true) @RequestParam String password2) {
|
||||
return R.status(userService.setPassword(user.getUserId(), password, password2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员修改密码
|
||||
*/
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.tenant.annotation.NonDS;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.system.pojo.dto.PhoneChangeDTO;
|
||||
import org.springblade.system.pojo.dto.PhoneVerifyDTO;
|
||||
import org.springblade.system.service.IUserPhoneService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 用户手机号变更(小程序「修改手机号」)
|
||||
* <p>
|
||||
* 对外路径:/blade-system/user/phone/** ;网关别名 /blade-user/phone/** 亦可到达。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@NonDS
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/user/phone")
|
||||
@Tag(name = "用户手机号", description = "修改手机号")
|
||||
public class UserPhoneController {
|
||||
|
||||
private final IUserPhoneService userPhoneService;
|
||||
|
||||
/**
|
||||
* 发送短信验证码(需登录)
|
||||
* <p>
|
||||
* 当前手机号、未占用的新手机号均可发送;新号若已被其他账号占用则拒绝。
|
||||
*/
|
||||
@PostMapping("/send-code")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "发送手机号变更验证码", description = "传入明文手机号,返回短信校验 id")
|
||||
public R sendCode(@Parameter(description = "手机号", required = true) @RequestParam String phone) {
|
||||
return userPhoneService.sendCode(phone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验原手机号验证码(修改手机号第 1 步)
|
||||
*/
|
||||
@PostMapping("/verify-old")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "校验原手机号验证码", description = "传入发送验证码返回的 id 与验证码")
|
||||
public R verifyOld(@Valid @RequestBody PhoneVerifyDTO phoneVerify) {
|
||||
return R.status(userPhoneService.verifyOldPhone(phoneVerify));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定新手机号(修改手机号第 3 步,需先完成 verify-old)
|
||||
*/
|
||||
@PostMapping("/change")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "更换手机号", description = "传入新手机号及短信校验 id、验证码")
|
||||
public R change(@Valid @RequestBody PhoneChangeDTO phoneChange) {
|
||||
return R.status(userPhoneService.changePhone(phoneChange));
|
||||
}
|
||||
|
||||
}
|
||||
+10
@@ -80,6 +80,9 @@ public interface DeptConvert {
|
||||
}
|
||||
// 公司所属公司编码就是他自己的部门编码
|
||||
dept.setBelongCompanyCode(dept.getDeptCode());
|
||||
// OA主键与上级OA主键,分批同步结束后用于重建层级
|
||||
dept.setOaId(company.getId());
|
||||
dept.setOaSupSubComId(company.getSupsubcomid());
|
||||
// 显示顺序处理
|
||||
dept.setSort(OAUtils.parseInt(company.getShoworder()));
|
||||
// 部门类型,公司
|
||||
@@ -100,6 +103,13 @@ public interface DeptConvert {
|
||||
Dept dept = this.baseConvert(oaDept);
|
||||
// 部门编码添加前缀
|
||||
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());
|
||||
String parentCode = dept.getParentCode();
|
||||
|
||||
+25
-3
@@ -64,11 +64,11 @@ public interface UserConvert {
|
||||
@Mapping(target = "password", ignore = true)
|
||||
@Mapping(target = "birthday", ignore = true)
|
||||
@Mapping(target = "sex", ignore = true)
|
||||
@Mapping(target = "account", ignore = true)
|
||||
@Mapping(source = "workcode", target = "code")
|
||||
@Mapping(source = "lastname", target = "name")
|
||||
@Mapping(source = "lastname", target = "realName")
|
||||
@Mapping(source = "mobile", target = "phone")
|
||||
@Mapping(source = "mobile", target = "account")
|
||||
@Mapping(source = "email", target = "email")
|
||||
User baseConvert(OAPersonResponse person);
|
||||
|
||||
@@ -86,7 +86,7 @@ public interface UserConvert {
|
||||
*/
|
||||
default User person2user(OAPersonResponse person, String defaultPassword) {
|
||||
User user = baseConvert(person);
|
||||
|
||||
user.setAccount(resolveAccount(person));
|
||||
// 密码
|
||||
user.setPassword(defaultPassword);
|
||||
// 性别
|
||||
@@ -102,6 +102,28 @@ public interface UserConvert {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析本系统登录账号:优先 OA loginid,其次工号,最后手机号
|
||||
*
|
||||
* @param person OA人员
|
||||
* @return 账号,无法识别时返回 null
|
||||
*/
|
||||
default String resolveAccount(OAPersonResponse person) {
|
||||
if (person == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(person.getLoginid())) {
|
||||
return person.getLoginid().trim();
|
||||
}
|
||||
if (StringUtils.isNotBlank(person.getWorkcode())) {
|
||||
return person.getWorkcode().trim();
|
||||
}
|
||||
if (StringUtils.isNotBlank(person.getMobile())) {
|
||||
return person.getMobile().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* oa人员转本系统用户部门
|
||||
* @param person
|
||||
@@ -120,7 +142,7 @@ public interface UserConvert {
|
||||
// 排序
|
||||
userDept.setSort(OAUtils.parseInt(person.getDsporder()));
|
||||
// 用户id
|
||||
userDept.setUserId(userMap.get(person.getMobile()));
|
||||
userDept.setUserId(userMap.get(resolveAccount(person)));
|
||||
userDept.setSyncTime(new Date());
|
||||
return userDept;
|
||||
}
|
||||
|
||||
+4
-5
@@ -35,7 +35,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 币种汇率 Excel
|
||||
@@ -62,8 +61,8 @@ public class CurrencyExcel implements Serializable {
|
||||
@ExcelProperty("汇率")
|
||||
private BigDecimal exchangeRate;
|
||||
|
||||
@ExcelProperty("生效日期")
|
||||
private LocalDate effectiveDate;
|
||||
@ExcelProperty(value = "生效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String effectiveDate;
|
||||
|
||||
@ExcelProperty("状态")
|
||||
private String statusName;
|
||||
@@ -71,8 +70,8 @@ public class CurrencyExcel implements Serializable {
|
||||
@ExcelProperty("来源")
|
||||
private String dataSource;
|
||||
|
||||
@ExcelProperty("失效日期")
|
||||
private LocalDate expiryDate;
|
||||
@ExcelProperty(value = "失效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String expiryDate;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
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.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* UserExcel
|
||||
@@ -102,7 +101,7 @@ public class UserExcel implements Serializable {
|
||||
private String postName;
|
||||
|
||||
@ColumnWidth(20)
|
||||
@ExcelProperty("生日")
|
||||
private Date birthday;
|
||||
@ExcelProperty(value = "生日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String birthday;
|
||||
|
||||
}
|
||||
|
||||
@@ -232,6 +232,12 @@ public class SysClient implements ISysClient {
|
||||
.orderByAsc(CargoType::getCargoCode)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(PERMISSIONS)
|
||||
public R<List<String>> getPermissions(String roleId) {
|
||||
return R.data(menuService.permissionCodes(roleId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(INVOICE_ITEMS)
|
||||
public R<List<InvoiceItem>> getInvoiceItems() {
|
||||
|
||||
@@ -133,4 +133,10 @@ public class UserClient implements IUserClient {
|
||||
return R.data(service.remove(Wrappers.<User>query().lambda().in(User::getTenantId, Func.toStrList(tenantIds))));
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(BIND_WX_MINI_OPENID)
|
||||
public R<Boolean> bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) {
|
||||
return R.data(service.bindWxMiniOpenId(tenantId, userId, openid, phone));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -13,6 +13,7 @@
|
||||
<result column="update_time" property="updateTime"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="is_deleted" property="isDeleted"/>
|
||||
<result column="unit_code" property="unitCode"/>
|
||||
<result column="unit_name" property="unitName"/>
|
||||
<result column="dimension" property="dimension"/>
|
||||
<result column="remark" property="remark"/>
|
||||
@@ -30,6 +31,7 @@
|
||||
mmu.update_time,
|
||||
mmu.status,
|
||||
mmu.is_deleted,
|
||||
mmu.unit_code,
|
||||
mmu.unit_name,
|
||||
mmu.dimension,
|
||||
mmu.remark
|
||||
@@ -39,6 +41,10 @@
|
||||
LEFT JOIN blade_user uu ON uu.id = mmu.update_user
|
||||
WHERE
|
||||
mmu.is_deleted = 0
|
||||
<if test="measurementUnit.unitCode != null and measurementUnit.unitCode != ''">
|
||||
<bind name="unitCodeLike" value="'%' + measurementUnit.unitCode + '%'"/>
|
||||
AND mmu.unit_code LIKE #{unitCodeLike}
|
||||
</if>
|
||||
<if test="measurementUnit.unitName != null and measurementUnit.unitName != ''">
|
||||
<bind name="unitNameLike" value="'%' + measurementUnit.unitName + '%'"/>
|
||||
AND mmu.unit_name LIKE #{unitNameLike}
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@
|
||||
</select>
|
||||
|
||||
<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 id="selectCustomerOptions" resultType="java.util.HashMap">
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.props;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* IAM账号同步配置。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "iam.sync")
|
||||
public class IamSyncProperties {
|
||||
|
||||
/** IAM增量账号接口地址。 */
|
||||
private String accountListUrl;
|
||||
|
||||
/** IAM组织接口地址。 */
|
||||
private String orgListUrl;
|
||||
|
||||
/** IAM接口Authorization请求头。 */
|
||||
private String authorization;
|
||||
|
||||
/** IAM接口Auth请求头。 */
|
||||
private String profileAuthorization;
|
||||
|
||||
/** 单页请求数量。 */
|
||||
private int pageSize = 50;
|
||||
|
||||
}
|
||||
+26
@@ -146,6 +146,13 @@ public interface IDeptService extends IService<Dept> {
|
||||
*/
|
||||
boolean removeDept(String ids);
|
||||
|
||||
/**
|
||||
* 删除非顶级组织,顶级组织(父级为 0)保留
|
||||
*
|
||||
* @return 删除数量
|
||||
*/
|
||||
int clearNonTopDepts();
|
||||
|
||||
/**
|
||||
* 新增或修改部门
|
||||
* <p>
|
||||
@@ -156,6 +163,13 @@ public interface IDeptService extends IService<Dept> {
|
||||
*/
|
||||
boolean submit(Dept dept);
|
||||
|
||||
/**
|
||||
* 从IAM同步管理租户组织。
|
||||
|
||||
* @return 同步处理的组织数量
|
||||
*/
|
||||
int syncIamOrganizations();
|
||||
|
||||
/**
|
||||
* 按名称与父级查询部门列表(限定当前会话租户)
|
||||
*
|
||||
@@ -180,4 +194,16 @@ public interface IDeptService extends IService<Dept> {
|
||||
*/
|
||||
void updateAncestors(Date startTime);
|
||||
|
||||
/**
|
||||
* 按 OA 主键与上级 OA 主键重建组织、部门父子关系,并刷新祖级列表
|
||||
*/
|
||||
void rebuildOaHierarchy();
|
||||
|
||||
/**
|
||||
* 删除 OA 同步后无法挂到上级的组织(上级 OA 主键在本地不存在),并级联删除其子级
|
||||
*
|
||||
* @return 删除数量
|
||||
*/
|
||||
int removeOaOrphansWithoutParent();
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -77,6 +77,14 @@ public interface IMenuService extends IService<Menu> {
|
||||
*/
|
||||
List<MenuVO> buttons(String roleId);
|
||||
|
||||
/**
|
||||
* 权限标识集合(按钮编号,与前端 GetButtons 叶子 code 一致)
|
||||
*
|
||||
* @param roleId 角色id
|
||||
* @return 权限标识
|
||||
*/
|
||||
List<String> permissionCodes(String roleId);
|
||||
|
||||
/**
|
||||
* 树形结构
|
||||
*
|
||||
|
||||
+37
@@ -1,5 +1,8 @@
|
||||
package org.springblade.system.service;
|
||||
|
||||
import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
|
||||
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
|
||||
|
||||
/**
|
||||
* oa同步接口
|
||||
* @author bfhuange
|
||||
@@ -18,4 +21,38 @@ public interface IOASyncService {
|
||||
* @param syncAll 是否同步所有
|
||||
*/
|
||||
void syncPersonAndPushMK(boolean syncAll);
|
||||
|
||||
/**
|
||||
* 从 OA 人员接口全量同步组织与人员,不推送 MK
|
||||
*
|
||||
* @return 处理的人员数量
|
||||
*/
|
||||
int syncPersonFromUserList();
|
||||
|
||||
/**
|
||||
* 按页从 OA 人员接口同步组织与人员
|
||||
*
|
||||
* @param current 当前页,从 1 开始
|
||||
* @param size 每页条数
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
OaPersonSyncPageVO syncPersonFromUserList(int current, int size);
|
||||
|
||||
/**
|
||||
* 按页从 OA 公司接口同步公司
|
||||
*
|
||||
* @param current 当前页,从 1 开始
|
||||
* @param size 每页条数
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
OaOrgSyncPageVO syncCompanyPage(int current, int size);
|
||||
|
||||
/**
|
||||
* 按页从 OA 部门接口同步部门;最后一页完成后更新祖级列表
|
||||
*
|
||||
* @param current 当前页,从 1 开始
|
||||
* @param size 每页条数
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
OaOrgSyncPageVO syncDepartmentPage(int current, int size);
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.system.pojo.dto.PhoneChangeDTO;
|
||||
import org.springblade.system.pojo.dto.PhoneVerifyDTO;
|
||||
|
||||
/**
|
||||
* 用户手机号变更服务
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IUserPhoneService {
|
||||
|
||||
/**
|
||||
* 发送变更手机号短信验证码
|
||||
*
|
||||
* @param phone 明文手机号
|
||||
* @return 含短信校验 id 的响应
|
||||
*/
|
||||
R sendCode(String phone);
|
||||
|
||||
/**
|
||||
* 校验原手机号验证码,通过后写入短期凭证
|
||||
*
|
||||
* @param phoneVerify 校验参数
|
||||
* @return 是否通过
|
||||
*/
|
||||
boolean verifyOldPhone(PhoneVerifyDTO phoneVerify);
|
||||
|
||||
/**
|
||||
* 校验新手机号验证码并更换手机号
|
||||
*
|
||||
* @param phoneChange 更换参数
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean changePhone(PhoneChangeDTO phoneChange);
|
||||
|
||||
}
|
||||
+13
-1
@@ -78,6 +78,13 @@ public interface IUserService extends BaseService<User> {
|
||||
*/
|
||||
boolean submit(User user);
|
||||
|
||||
/**
|
||||
* 从IAM同步管理租户账号。
|
||||
*
|
||||
* @return 同步处理的账号数量
|
||||
*/
|
||||
int syncIamAccounts();
|
||||
|
||||
/**
|
||||
* 修改用户(租户守卫校验用户归属,含账号 / 手机查重)
|
||||
*
|
||||
@@ -181,6 +188,11 @@ public interface IUserService extends BaseService<User> {
|
||||
*/
|
||||
UserInfo userInfo(UserOauth userOauth);
|
||||
|
||||
/**
|
||||
* 绑定微信小程序 openid 到已有用户(blade_user_oauth,source=WECHAT_MINI)
|
||||
*/
|
||||
boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone);
|
||||
|
||||
/**
|
||||
* 根据租户与账号获取用户
|
||||
*
|
||||
@@ -281,7 +293,7 @@ public interface IUserService extends BaseService<User> {
|
||||
boolean registerUser(User user);
|
||||
|
||||
/**
|
||||
* 新建IAM统一身份认证用户(按可信租户落库)
|
||||
* 新建或补齐IAM统一身份认证用户(按可信租户落库,默认分配角色 1123598816738675203)
|
||||
*
|
||||
* @param user 用户实体
|
||||
* @return 是否成功
|
||||
|
||||
+2
@@ -125,6 +125,8 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
|
||||
CurrencyExcel excel = data.get(index);
|
||||
try {
|
||||
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.setStatus(STATUS_ENABLED);
|
||||
prepare(currency, SOURCE_BATCH);
|
||||
|
||||
+479
-29
@@ -29,8 +29,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.common.constant.DataStatusEnum;
|
||||
import org.springblade.core.cache.utils.CacheUtil;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
@@ -42,16 +46,26 @@ import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.mapper.DeptMapper;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
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.UserVO;
|
||||
import org.springblade.system.props.IamSyncProperties;
|
||||
import org.springblade.system.service.IDeptService;
|
||||
import org.springblade.system.service.IUserService;
|
||||
import org.springblade.system.wrapper.DeptWrapper;
|
||||
import org.springblade.system.wrapper.UserWrapper;
|
||||
import org.springblade.thirdparty.oa.constant.OAConstant;
|
||||
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -59,6 +73,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT;
|
||||
import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT_PARENT;
|
||||
import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
|
||||
|
||||
/**
|
||||
* 服务实现类
|
||||
@@ -72,8 +87,13 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
|
||||
private static final String TENANT_ID = "tenantId";
|
||||
private static final String PARENT_ID = "parentId";
|
||||
private static final String IAM_SYNC_TENANT_ID = "000000";
|
||||
private static final Long IAM_SYNC_PARENT_ID = 1123598813738675201L;
|
||||
private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
||||
|
||||
private final IUserService userService;
|
||||
private final IamSyncProperties iamSyncProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public Dept getDetail(Dept dept) {
|
||||
@@ -214,6 +234,21 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(Dept dept) {
|
||||
@@ -255,9 +290,190 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
return saveOrUpdate(dept);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int syncIamOrganizations() {
|
||||
int pageNumber = 1;
|
||||
int fetchedCount = 0;
|
||||
int syncedCount = 0;
|
||||
int totalCount = -1;
|
||||
int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50;
|
||||
while (true) {
|
||||
JsonNode dataNode = requestIamOrgPage(pageNumber, pageSize);
|
||||
JsonNode orgList = dataNode.path("list");
|
||||
if (!orgList.isArray() || orgList.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
if (dataNode.has("total")) {
|
||||
totalCount = dataNode.path("total").asInt(totalCount);
|
||||
}
|
||||
for (JsonNode orgNode : orgList) {
|
||||
if (syncIamOrganization(orgNode)) {
|
||||
syncedCount++;
|
||||
}
|
||||
}
|
||||
fetchedCount += orgList.size();
|
||||
int responsePage = dataNode.path("page").asInt(pageNumber);
|
||||
int responseSize = dataNode.path("size").asInt(pageSize);
|
||||
if ((totalCount >= 0 && fetchedCount >= totalCount)
|
||||
|| orgList.size() < pageSize
|
||||
|| (totalCount >= 0 && responsePage * responseSize >= totalCount)) {
|
||||
break;
|
||||
}
|
||||
pageNumber = responsePage + 1;
|
||||
}
|
||||
log.info("IAM组织同步完成,tenantId={}, fetchedCount={}, syncedCount={}", IAM_SYNC_TENANT_ID, fetchedCount, syncedCount);
|
||||
return syncedCount;
|
||||
}
|
||||
|
||||
private JsonNode requestIamOrgPage(int pageNumber, int pageSize) {
|
||||
try {
|
||||
Map<String, String> requestBody = new LinkedHashMap<>();
|
||||
requestBody.put("size", String.valueOf(pageSize));
|
||||
requestBody.put("page", String.valueOf(pageNumber));
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(iamSyncProperties.getOrgListUrl()))
|
||||
.timeout(Duration.ofSeconds(20))
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization()))
|
||||
.header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization()))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8))
|
||||
.build();
|
||||
HttpResponse<String> response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new ServiceException(StringUtil.format("IAM组织接口调用失败,HTTP状态码:{}", response.statusCode()));
|
||||
}
|
||||
JsonNode responseNode = objectMapper.readTree(response.body());
|
||||
if (!"0".equals(responseNode.path("code").asText())) {
|
||||
throw new ServiceException(StringUtil.format("IAM组织接口调用失败:{}", responseNode.path("msg").asText()));
|
||||
}
|
||||
JsonNode dataNode = responseNode.path("data");
|
||||
if (!dataNode.isObject()) {
|
||||
throw new ServiceException("IAM组织接口返回数据格式错误");
|
||||
}
|
||||
return dataNode;
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("调用IAM组织接口被中断,page={}", pageNumber, exception);
|
||||
throw new ServiceException("调用IAM组织接口被中断");
|
||||
} catch (IOException | IllegalArgumentException exception) {
|
||||
log.error("调用IAM组织接口失败,page={}", pageNumber, exception);
|
||||
throw new ServiceException("调用IAM组织接口失败");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean syncIamOrganization(JsonNode orgNode) {
|
||||
String orgCode = readIamText(orgNode, "orgCode", "org_code", "organizationCode", "organization_code",
|
||||
"code", "app_org__org_code", "app_org__org_no", "app_org__organization_code", "app_org__code");
|
||||
String orgId = readIamText(orgNode, "orgId", "org_id", "id", "app_org__id", "app_org__org_id");
|
||||
if (StringUtil.isBlank(orgCode)) {
|
||||
orgCode = orgId;
|
||||
}
|
||||
if (StringUtil.isBlank(orgCode)) {
|
||||
log.warn("IAM组织缺少组织编码,跳过同步");
|
||||
return false;
|
||||
}
|
||||
if (orgCode.length() > 30) {
|
||||
log.warn("IAM组织编码超过30个字符,跳过同步,orgCode={}", orgCode);
|
||||
return false;
|
||||
}
|
||||
String orgName = readIamText(orgNode, "name", "orgName", "org_name", "organizationName", "organization_name",
|
||||
"fullName", "app_org__name", "app_org__org_name", "app_org__org_full_name", "app_org__organization_name");
|
||||
if (StringUtil.isBlank(orgName)) {
|
||||
log.warn("IAM组织缺少组织名称,跳过同步,orgCode={}", orgCode);
|
||||
return false;
|
||||
}
|
||||
Integer status = readIamInt(orgNode, "status", "org_status", "app_org__status", "app_org__org_status") == 1
|
||||
? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode();
|
||||
Dept dept = getOne(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getTenantId, IAM_SYNC_TENANT_ID)
|
||||
.eq(Dept::getDeptCode, orgCode), false);
|
||||
if (dept == null) {
|
||||
dept = new Dept();
|
||||
dept.setTenantId(IAM_SYNC_TENANT_ID);
|
||||
dept.setParentId(IAM_SYNC_PARENT_ID);
|
||||
dept.setAncestors(resolveIamParentAncestors());
|
||||
dept.setDeptCode(orgCode);
|
||||
dept.setDeptName(orgName);
|
||||
dept.setFullName(orgName);
|
||||
dept.setShortName(orgName);
|
||||
dept.setDeptCategory(1);
|
||||
dept.setSort(0);
|
||||
dept.setStatus(status);
|
||||
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
dept.setIsOa(1);
|
||||
dept.setIsPlatformCompany(0);
|
||||
dept.setSyncTime(new Date());
|
||||
boolean saved = save(dept);
|
||||
if (saved) {
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
boolean changed = !Objects.equals(dept.getDeptName(), orgName) || !Objects.equals(dept.getFullName(), orgName)
|
||||
|| !Objects.equals(dept.getShortName(), orgName) || !Objects.equals(dept.getStatus(), status)
|
||||
|| !Objects.equals(dept.getIsOa(), 1);
|
||||
if (!changed) {
|
||||
return true;
|
||||
}
|
||||
dept.setDeptName(orgName);
|
||||
dept.setFullName(orgName);
|
||||
dept.setShortName(orgName);
|
||||
dept.setStatus(status);
|
||||
dept.setIsOa(1);
|
||||
dept.setSyncTime(new Date());
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
return updateById(dept);
|
||||
}
|
||||
|
||||
private String resolveIamParentAncestors() {
|
||||
Dept parent = getById(IAM_SYNC_PARENT_ID);
|
||||
String ancestors = parent == null ? String.valueOf(BladeConstant.TOP_PARENT_ID) : parent.getAncestors();
|
||||
if (StringUtil.isBlank(ancestors)) {
|
||||
ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID);
|
||||
}
|
||||
return ancestors + StringPool.COMMA + IAM_SYNC_PARENT_ID;
|
||||
}
|
||||
|
||||
private String readIamText(JsonNode node, String... fieldNames) {
|
||||
JsonNode valueNode = findIamNode(node, fieldNames);
|
||||
return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim();
|
||||
}
|
||||
|
||||
private int readIamInt(JsonNode node, String... fieldNames) {
|
||||
JsonNode valueNode = findIamNode(node, fieldNames);
|
||||
return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0);
|
||||
}
|
||||
|
||||
private JsonNode findIamNode(JsonNode node, String... fieldNames) {
|
||||
for (String fieldName : fieldNames) {
|
||||
JsonNode valueNode = node.get(fieldName);
|
||||
if (valueNode != null && !valueNode.isNull()) {
|
||||
return valueNode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String normalizeAuthorizationHeader(String value) {
|
||||
if (StringUtil.isBlank(value)) {
|
||||
return StringPool.EMPTY;
|
||||
}
|
||||
if (StringUtil.startsWithIgnoreCase(value, "Basic ") || StringUtil.startsWithIgnoreCase(value, "Bearer ")) {
|
||||
return value;
|
||||
}
|
||||
return "Basic " + value;
|
||||
}
|
||||
|
||||
private void validateDeptCategory(Dept dept, Dept parent) {
|
||||
// 顶级组织不要求选择上级组织
|
||||
if (parent == null) {
|
||||
throw new ServiceException("请选择上级组织");
|
||||
if (Integer.valueOf(6).equals(dept.getDeptCategory()) && Func.isEmpty(dept.getCarrierCustomerId())) {
|
||||
throw new ServiceException("请选择承运商");
|
||||
}
|
||||
return;
|
||||
}
|
||||
List<Integer> allowedCategories;
|
||||
if (BladeConstant.TOP_PARENT_ID.equals(parent.getParentId())) {
|
||||
@@ -288,13 +504,24 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
if (deptCode.length() > 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("请选择已配置组织编码的上级组织");
|
||||
}
|
||||
String pattern = Pattern.quote(parent.getDeptCode()) + "-\\d+";
|
||||
if (!deptCode.matches(pattern)) {
|
||||
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);
|
||||
if (Func.isNotEmpty(dept.getId())) {
|
||||
queryWrapper.ne(Dept::getId, dept.getId());
|
||||
@@ -302,7 +529,6 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
if (baseMapper.selectCount(queryWrapper) > 0) {
|
||||
throw new ServiceException("组织编码已存在");
|
||||
}
|
||||
dept.setDeptCode(deptCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -345,24 +571,243 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
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)
|
||||
@Override
|
||||
public void updateAncestors(Date startTime) {
|
||||
// 1. 查询需要更新祖级列表的部门
|
||||
List<Dept> list = this.list(Wrappers.<Dept>lambdaQuery()
|
||||
// 祖级列表为空
|
||||
this.refreshAncestors(false, startTime);
|
||||
}
|
||||
|
||||
@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)
|
||||
// 开始时间不为空,查询同步时间大于开始时间或修改时间大于开始时间的数据
|
||||
.or(startTime != null, wrapper -> wrapper.ge(Dept::getSyncTime, startTime))
|
||||
.or(startTime != null, wrapper -> wrapper.ge(Dept::getUpdateTime, startTime))
|
||||
);
|
||||
@@ -370,29 +815,19 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
log.info("需要更新祖级列表的部门为空");
|
||||
return;
|
||||
}
|
||||
// 所有公司部门列表
|
||||
List<DeptVO> allList = this.list().stream()
|
||||
List<DeptVO> allList = (refreshAll ? list : this.list()).stream()
|
||||
.map(dept -> BeanUtil.copyProperties(dept, DeptVO.class))
|
||||
.toList();
|
||||
// id对应的部门map
|
||||
Map<Long, DeptVO> idDeptMap = allList.stream()
|
||||
.collect(Collectors.toMap(DeptVO::getId, Function.identity(), (a, b) -> b));
|
||||
// 获取根节点列表
|
||||
List<DeptVO> rootList = getRootList(allList, idDeptMap);
|
||||
// 计算祖级列表
|
||||
setAncestors(rootList);
|
||||
|
||||
// 3. 更新祖级列表
|
||||
List<Dept> updateParams = list.stream()
|
||||
.filter(dept -> {
|
||||
DeptVO vo = idDeptMap.get(dept.getId());
|
||||
if (vo == null) {
|
||||
if (vo == null || vo.getAncestors() == null) {
|
||||
return false;
|
||||
}
|
||||
if (vo.getAncestors() == null) {
|
||||
return false;
|
||||
}
|
||||
// 祖级列表和数据库的不一样的才需要更新
|
||||
return !StringUtil.equals(vo.getAncestors(), dept.getAncestors());
|
||||
})
|
||||
.map(dept -> {
|
||||
@@ -407,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
|
||||
|
||||
+21
@@ -46,6 +46,7 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
|
||||
|
||||
private static final int STATUS_ENABLED = 1;
|
||||
private static final int STATUS_DISABLED = 2;
|
||||
private static final int UNIT_CODE_MAX_LENGTH = 50;
|
||||
private static final int UNIT_NAME_MAX_LENGTH = 50;
|
||||
private static final int DIMENSION_MAX_LENGTH = 20;
|
||||
private static final int REMARK_MAX_LENGTH = 200;
|
||||
@@ -85,6 +86,7 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
|
||||
}
|
||||
|
||||
private void prepare(MeasurementUnit measurementUnit) {
|
||||
measurementUnit.setUnitCode(trimToEmpty(measurementUnit.getUnitCode()));
|
||||
measurementUnit.setUnitName(trimToEmpty(measurementUnit.getUnitName()));
|
||||
measurementUnit.setDimension(trimToEmpty(measurementUnit.getDimension()));
|
||||
measurementUnit.setRemark(trimToNull(measurementUnit.getRemark()));
|
||||
@@ -94,6 +96,12 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
|
||||
}
|
||||
|
||||
private void validate(MeasurementUnit measurementUnit) {
|
||||
if (Func.isEmpty(measurementUnit.getUnitCode())) {
|
||||
throw new ServiceException("计量单位编码不能为空");
|
||||
}
|
||||
if (measurementUnit.getUnitCode().length() > UNIT_CODE_MAX_LENGTH) {
|
||||
throw new ServiceException("计量单位编码不能超过50字");
|
||||
}
|
||||
if (Func.isEmpty(measurementUnit.getUnitName())) {
|
||||
throw new ServiceException("计量单位不能为空");
|
||||
}
|
||||
@@ -115,9 +123,22 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
|
||||
&& !Objects.equals(measurementUnit.getStatus(), STATUS_DISABLED)) {
|
||||
throw new ServiceException("启停状态不正确");
|
||||
}
|
||||
validateUniqueUnitCode(measurementUnit);
|
||||
validateUniqueUnitName(measurementUnit);
|
||||
}
|
||||
|
||||
private void validateUniqueUnitCode(MeasurementUnit measurementUnit) {
|
||||
LambdaQueryWrapper<MeasurementUnit> queryWrapper = Wrappers.<MeasurementUnit>lambdaQuery()
|
||||
.eq(MeasurementUnit::getUnitCode, measurementUnit.getUnitCode())
|
||||
.eq(MeasurementUnit::getIsDeleted, 0);
|
||||
if (Func.isNotEmpty(measurementUnit.getId())) {
|
||||
queryWrapper.ne(MeasurementUnit::getId, measurementUnit.getId());
|
||||
}
|
||||
if (count(queryWrapper) > 0L) {
|
||||
throw new ServiceException("该计量单位编码已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateUniqueUnitName(MeasurementUnit measurementUnit) {
|
||||
LambdaQueryWrapper<MeasurementUnit> queryWrapper = Wrappers.<MeasurementUnit>lambdaQuery()
|
||||
.eq(MeasurementUnit::getUnitName, measurementUnit.getUnitName())
|
||||
|
||||
+29
@@ -164,6 +164,35 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IM
|
||||
return menuWrapper.listNodeVO(buttons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> permissionCodes(String roleId) {
|
||||
List<String> permissionCodes = new ArrayList<>();
|
||||
// Feign 调用时无登录态,不能走 AuthUtil.isAdministrator() 分支;按 roleId 取按钮权限
|
||||
List<Menu> buttons = StringUtil.isBlank(roleId)
|
||||
? Collections.emptyList()
|
||||
: baseMapper.buttons(Func.toLongList(roleId));
|
||||
MenuWrapper menuWrapper = new MenuWrapper();
|
||||
collectLeafPermissionCodes(menuWrapper.listNodeVO(buttons), permissionCodes);
|
||||
return permissionCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集按钮树叶子节点的权限编号(与前端 SET_PERMISSION 逻辑一致)
|
||||
*/
|
||||
private void collectLeafPermissionCodes(List<MenuVO> menuList, List<String> permissionCodes) {
|
||||
if (menuList == null || menuList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (MenuVO menu : menuList) {
|
||||
List<MenuVO> children = menu.getChildren();
|
||||
if (children != null && !children.isEmpty()) {
|
||||
collectLeafPermissionCodes(children, permissionCodes);
|
||||
} else if (StringUtil.isNotBlank(menu.getCode())) {
|
||||
permissionCodes.add(menu.getCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TreeNode> tree() {
|
||||
return ForestNodeMerger.merge(baseMapper.tree());
|
||||
|
||||
+430
-248
@@ -11,16 +11,15 @@ import org.springblade.common.constant.DictTypeEnum;
|
||||
import org.springblade.core.cache.utils.CacheUtil;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.DigestUtil;
|
||||
import org.springblade.system.cache.DictCache;
|
||||
import org.springblade.system.cache.ParamCache;
|
||||
import org.springblade.system.convert.DeptConvert;
|
||||
import org.springblade.system.convert.UserConvert;
|
||||
import org.springblade.system.log.ComposeLogUtil;
|
||||
import org.springblade.system.pojo.entity.*;
|
||||
import org.springblade.system.pojo.enums.DataSync;
|
||||
import org.springblade.system.pojo.enums.DeptCategory;
|
||||
import org.springblade.system.pojo.vo.UserDeptIdsVO;
|
||||
import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
|
||||
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
|
||||
import org.springblade.system.service.*;
|
||||
import org.springblade.system.util.DataSyncRecordUtils;
|
||||
import org.springblade.thirdparty.oa.constant.OAConstant;
|
||||
@@ -29,6 +28,8 @@ import org.springblade.thirdparty.oa.feign.IOAClient;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OACompanyResponse;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OADepartmentResponse;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OAResponse;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OAResponseData;
|
||||
import org.springblade.thirdparty.oa.pojo.search.OACompanySearch;
|
||||
import org.springblade.thirdparty.oa.pojo.search.OADepartmentSearch;
|
||||
import org.springblade.thirdparty.oa.pojo.search.OAPersonSearch;
|
||||
@@ -38,6 +39,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -54,20 +56,14 @@ import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class OASyncServiceImpl implements IOASyncService {
|
||||
/**
|
||||
* 用户表部门id最大长度
|
||||
*/
|
||||
private static final int MAX_DEPT_ID_LENGTH = 2000;
|
||||
|
||||
private final IOAClient oaClient;
|
||||
private final DeptConvert deptConvert;
|
||||
private final IDeptService deptService;
|
||||
private final IUserService userService;
|
||||
private final UserConvert userConvert;
|
||||
private final IUserDeptService userDeptService;
|
||||
private final IRoleService roleService;
|
||||
private final IMKPushService mkPushService;
|
||||
private final IDataSyncRecordService dataSyncRecordService;
|
||||
private final OaUserListSyncHelper oaUserListSyncHelper;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
@@ -78,10 +74,18 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
Date startTime = this.syncAndRecord(DataSyncRecordUtils::createOAOrgFetch, queryStartTime -> {
|
||||
// 1. 同步公司
|
||||
this.syncCompany(queryStartTime);
|
||||
// 公司分批入库后,重建层级并清理无法挂载的公司
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
// 2. 同步部门
|
||||
this.syncDept(queryStartTime);
|
||||
// 2. 更新部门祖级列表
|
||||
// 3. 分批写入完成后,按 OA 主键重建组织、部门层级
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
// 4. 更新部门祖级列表
|
||||
deptService.updateAncestors(queryStartTime);
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
}, syncAll);
|
||||
// 2. 推送mk
|
||||
mkPushService.pushOrgAndRecord(startTime);
|
||||
@@ -104,6 +108,53 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public int syncPersonFromUserList() {
|
||||
AtomicInteger syncedCount = new AtomicInteger();
|
||||
try {
|
||||
ComposeLogUtil.addLog(log);
|
||||
this.syncAndRecord(DataSyncRecordUtils::createOAPersonFetch, startTime ->
|
||||
syncedCount.set(this.syncPersonFromOa(null)), true);
|
||||
return syncedCount.get();
|
||||
} finally {
|
||||
ComposeLogUtil.removeLastLog();
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public OaPersonSyncPageVO syncPersonFromUserList(int current, int size) {
|
||||
try {
|
||||
ComposeLogUtil.addLog(log);
|
||||
return this.syncPersonFromOaPage(current, size);
|
||||
} finally {
|
||||
ComposeLogUtil.removeLastLog();
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public OaOrgSyncPageVO syncCompanyPage(int current, int size) {
|
||||
try {
|
||||
ComposeLogUtil.addLog(log);
|
||||
return this.syncCompanyFromOaPage(current, size);
|
||||
} finally {
|
||||
ComposeLogUtil.removeLastLog();
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public OaOrgSyncPageVO syncDepartmentPage(int current, int size) {
|
||||
try {
|
||||
ComposeLogUtil.addLog(log);
|
||||
return this.syncDepartmentFromOaPage(current, size);
|
||||
} finally {
|
||||
ComposeLogUtil.removeLastLog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步并记录
|
||||
*
|
||||
@@ -146,20 +197,15 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
// 未处理的数据
|
||||
List<Dept> notHandleList = new ArrayList<>();
|
||||
// 1. 设置查询参数
|
||||
OACompanySearch companySearch = new OACompanySearch();
|
||||
companySearch.setCurPage(1);
|
||||
if (startTime != null) {
|
||||
// 开始时间不为空,设置修改时间参数
|
||||
companySearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
}
|
||||
OACompanySearch companySearch = buildCompanySearch(startTime);
|
||||
// 2. 分页查询并处理数据
|
||||
OAUtils.pageSyncHandler(companySearch, param -> oaClient.queryCompanyPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询公司信息失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询公司信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||
// 处理数据
|
||||
List<Dept> deptList = handleCompany(list);
|
||||
notHandleList.addAll(deptList);
|
||||
OrgSyncCount syncCount = handleCompany(list, false);
|
||||
notHandleList.addAll(syncCount.getNotHandledList());
|
||||
});
|
||||
// 3. 未处理的数据
|
||||
if (CollectionUtil.isNotEmpty(notHandleList)) {
|
||||
@@ -175,7 +221,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
* @param startTime 查询开始时间
|
||||
*/
|
||||
private void syncDept(Date startTime) {
|
||||
String subCompanyIds = getSubCompanyIds();
|
||||
String subCompanyIds = getOaSubCompanyIds(false);
|
||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
|
||||
return;
|
||||
@@ -183,21 +229,15 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
// 未处理的数据
|
||||
List<Dept> notHandleList = new ArrayList<>();
|
||||
// 1. 设置查询参数
|
||||
OADepartmentSearch departmentSearch = new OADepartmentSearch();
|
||||
departmentSearch.setCurPage(1);
|
||||
departmentSearch.setSubcompanyid1(subCompanyIds);
|
||||
if (startTime != null) {
|
||||
// 开始时间不为空,设置修改时间参数
|
||||
departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
}
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, subCompanyIds);
|
||||
// 2. 分页查询并处理数据
|
||||
OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询部门信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||
// 处理数据
|
||||
List<Dept> deptList = handleDept(list);
|
||||
notHandleList.addAll(deptList);
|
||||
OrgSyncCount syncCount = handleDept(list, false);
|
||||
notHandleList.addAll(syncCount.getNotHandledList());
|
||||
});
|
||||
// 3. 未处理的数据
|
||||
if (CollectionUtil.isNotEmpty(notHandleList)) {
|
||||
@@ -213,242 +253,325 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
* @param startTime 查询开始时间
|
||||
*/
|
||||
private void syncPerson(Date startTime) {
|
||||
String subCompanyIds = getSubCompanyIds();
|
||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
|
||||
return;
|
||||
}
|
||||
// 1. 设置查询参数
|
||||
OAPersonSearch personSearch = new OAPersonSearch();
|
||||
personSearch.setCurPage(1);
|
||||
personSearch.setSubcompanyid1(subCompanyIds);
|
||||
if (startTime != null) {
|
||||
// 开始时间不为空,设置修改时间参数
|
||||
personSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
}
|
||||
// 2. 分页查询并处理数据
|
||||
OAUtils.pageSyncHandler(personSearch, param -> oaClient.queryPersonPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询人员信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(this::handlePerson);
|
||||
// 清除用户缓存
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
this.syncPersonFromOa(startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理oa人员
|
||||
* @param oaPersons
|
||||
* 从 OA 人员列表同步组织与人员
|
||||
*
|
||||
* @param startTime 增量查询开始时间,为空则全量
|
||||
* @return 处理的人员数量
|
||||
*/
|
||||
private void handlePerson(List<OAPersonResponse> oaPersons) {
|
||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||
// 数据为空,直接返回
|
||||
return;
|
||||
}
|
||||
// 根公司id
|
||||
String rootCompanyId = getRootCompanyId();
|
||||
if (rootCompanyId == null) {
|
||||
return;
|
||||
}
|
||||
// 根公司下要同步的部门id
|
||||
Set<String> rootCompanyDeptIds = getRootCompanyDeptIds(rootCompanyId);
|
||||
oaPersons = oaPersons.stream()
|
||||
// 公司不是根公司,或者部门在根公司下要同步的部门列表中
|
||||
.filter(oaPerson -> !rootCompanyId.equals(oaPerson.getSubcompanyid1()) || rootCompanyDeptIds.contains(oaPerson.getDepartmentid()))
|
||||
.toList();
|
||||
// 根据手机号转成set
|
||||
TreeSet<OAPersonResponse> oaPersonSet = CollectionUtil.toTreeSet(oaPersons, Comparator.comparing(OAPersonResponse::getMobile));
|
||||
// 默认密码
|
||||
String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
||||
// 转换数据
|
||||
List<User> users = oaPersonSet.stream()
|
||||
// 只需要手机不为空的
|
||||
.filter(person -> StringUtils.isNotBlank(person.getMobile()))
|
||||
.map(person -> userConvert.person2user(person, defaultPassword))
|
||||
.toList();
|
||||
List<String> phones = users.stream()
|
||||
.map(User::getPhone)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
// 查询所有用户
|
||||
Map<String, Long> userMap = userService.list(Wrappers.<User>lambdaQuery()
|
||||
//.eq(User::getIsDeleted, BladeConstant.DB_NOT_DELETED)
|
||||
.in(User::getPhone, phones)
|
||||
).stream()
|
||||
// 解密手机号
|
||||
.peek(userService::decryptPhone)
|
||||
.collect(Collectors.toMap(User::getPhone, User::getId, (a, b) -> b));
|
||||
// 数据库存在的所有用户id
|
||||
Set<Long> existsUserIds = new HashSet<>(userMap.values());
|
||||
users.forEach(user -> {
|
||||
if (userMap.containsKey(user.getPhone())) {
|
||||
// 根据手机号获取对应的用户id
|
||||
user.setId(userMap.get(user.getPhone()));
|
||||
// 清空密码,不修改密码
|
||||
user.setPassword(null);
|
||||
} else {
|
||||
// 没有就生成一个id
|
||||
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
userService.encryptPhone(user);
|
||||
userMap.put(user.getPhone(), user.getId());
|
||||
}
|
||||
});
|
||||
private int syncPersonFromOa(Date startTime) {
|
||||
OAPersonSearch personSearch = buildPersonSearch(startTime);
|
||||
List<OAPersonResponse> oaPersons = new ArrayList<>();
|
||||
OAUtils.pageSyncHandler(personSearch, param -> oaClient.queryPersonPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询人员信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(oaPersons::addAll);
|
||||
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
|
||||
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
return personSyncCount.getSyncedCount();
|
||||
}
|
||||
|
||||
// 不存在的新增
|
||||
List<User> addUsers = users.stream()
|
||||
.filter(user -> !existsUserIds.contains(user.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(addUsers)) {
|
||||
ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size());
|
||||
userService.saveBatch(addUsers);
|
||||
/**
|
||||
* 按页从 OA 人员列表同步组织与人员
|
||||
*
|
||||
* @param current 当前页
|
||||
* @param size 每页条数
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
OAPersonSearch personSearch = buildPersonSearch(null);
|
||||
personSearch.setCurPage(pageNo);
|
||||
personSearch.setPageSize(pageSize);
|
||||
OAResponse<OAPersonResponse> oaResponse = oaClient.queryPersonPage(new OASearch<>(personSearch));
|
||||
if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询人员信息失败 {}", JSON.toJSONString(oaResponse));
|
||||
throw new ServiceException("调用OA接口查询人员信息失败");
|
||||
}
|
||||
// 存在的修改
|
||||
List<User> updateUsers = users.stream()
|
||||
.filter(user -> existsUserIds.contains(user.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(updateUsers)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
|
||||
userService.updateBatchById(updateUsers);
|
||||
}
|
||||
// 没有手机号的数据 = 手机号为空的数量
|
||||
long noPhoneNum = oaPersons.stream()
|
||||
.map(OAPersonResponse::getMobile)
|
||||
.filter(StringUtils::isBlank)
|
||||
.count();
|
||||
ComposeLogUtil.getLastLog().info("没有手机号的数据:{}", noPhoneNum);
|
||||
OAResponseData<OAPersonResponse> responseData = oaResponse.getData();
|
||||
List<OAPersonResponse> oaPersons = responseData.getDataList() == null
|
||||
? Collections.emptyList() : responseData.getDataList();
|
||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
|
||||
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
OaPersonSyncPageVO pageVO = new OaPersonSyncPageVO();
|
||||
pageVO.setCurrent(pageNo);
|
||||
pageVO.setSize(pageSize);
|
||||
pageVO.setTotal(totalSize);
|
||||
pageVO.setFetchedCount(oaPersons.size());
|
||||
pageVO.setSyncedCount(personSyncCount.getSyncedCount());
|
||||
pageVO.setSkippedCount(personSyncCount.getSkippedCount());
|
||||
boolean finished = oaPersons.isEmpty()
|
||||
|| oaPersons.size() < pageSize
|
||||
|| (long) pageNo * pageSize >= totalSize;
|
||||
pageVO.setFinished(finished);
|
||||
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 {}/{},成功{},跳过{}",
|
||||
pageNo, totalSize, personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount());
|
||||
return pageVO;
|
||||
}
|
||||
|
||||
Map<String, Long> userDeptMap = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||
.in(UserDept::getUserId, userMap.values())
|
||||
).stream()
|
||||
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (a, b) -> b));
|
||||
// 数据库存在的所有用户部门id
|
||||
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
|
||||
/**
|
||||
* 按页从 OA 公司接口同步公司
|
||||
*
|
||||
* @param current 当前页
|
||||
* @param size 每页条数
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
OACompanySearch companySearch = buildCompanySearch(null);
|
||||
companySearch.setCurPage(pageNo);
|
||||
companySearch.setPageSize(pageSize);
|
||||
OAResponse<OACompanyResponse> oaResponse = oaClient.queryCompanyPage(new OASearch<>(companySearch));
|
||||
if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询公司信息失败 {}", JSON.toJSONString(oaResponse));
|
||||
throw new ServiceException("调用OA接口查询公司信息失败");
|
||||
}
|
||||
OAResponseData<OACompanyResponse> responseData = oaResponse.getData();
|
||||
List<OACompanyResponse> oaCompanies = responseData.getDataList() == null
|
||||
? Collections.emptyList() : responseData.getDataList();
|
||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||
OrgSyncCount syncCount = handleCompany(oaCompanies, true);
|
||||
if (CollectionUtil.isNotEmpty(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, Boolean.FALSE);
|
||||
ComposeLogUtil.getLastLog().info("OA公司分页同步完成 {}/{},成功{},跳过{}",
|
||||
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||
return pageVO;
|
||||
}
|
||||
|
||||
List<UserDept> userDeptList = oaPersons.stream()
|
||||
// 只要包含用户手机号的
|
||||
.filter(oaPerson -> userMap.containsKey(oaPerson.getMobile()))
|
||||
.map(oaPerson -> userConvert.person2userDept(oaPerson, userMap))
|
||||
.toList();
|
||||
userDeptList.forEach(userDept -> {
|
||||
String userDeptKey = getUserDeptKey(userDept);
|
||||
if (userDeptMap.containsKey(userDeptKey)) {
|
||||
// 根据key获取用户部门id
|
||||
userDept.setId(userDeptMap.get(userDeptKey));
|
||||
} else {
|
||||
// 没有就生成一个id
|
||||
userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
}
|
||||
});
|
||||
// 不存在的新增
|
||||
List<UserDept> addList = userDeptList.stream()
|
||||
.filter(userDept -> !existsUserDeptIds.contains(userDept.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(addList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量新增用户部门:{}", addList.size());
|
||||
userDeptService.saveBatch(addList);
|
||||
/**
|
||||
* 按页从 OA 部门接口同步部门
|
||||
*
|
||||
* @param current 当前页
|
||||
* @param size 每页条数
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
String subCompanyIds = getOaSubCompanyIds(true);
|
||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步");
|
||||
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
|
||||
emptyPageVO.setFinished(true);
|
||||
return emptyPageVO;
|
||||
}
|
||||
// 存在的修改
|
||||
List<UserDept> updateList = userDeptList.stream()
|
||||
.filter(userDept -> existsUserDeptIds.contains(userDept.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size());
|
||||
userDeptService.updateBatchById(updateList);
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(null, subCompanyIds);
|
||||
departmentSearch.setCurPage(pageNo);
|
||||
departmentSearch.setPageSize(pageSize);
|
||||
OAResponse<OADepartmentResponse> oaResponse = oaClient.queryDepartmentPage(new OASearch<>(departmentSearch));
|
||||
if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(oaResponse));
|
||||
throw new ServiceException("调用OA接口查询部门信息失败");
|
||||
}
|
||||
// 回写部门id到用户表
|
||||
Collection<Long> userIds = userMap.values();
|
||||
List<UserDeptIdsVO> list = userDeptService.queryUserDeptIds(userIds);
|
||||
// 查询没有角色的用户id
|
||||
Set<Long> noRoleUserIds = userService.list(Wrappers.<User>lambdaQuery()
|
||||
.in(User::getId, userIds)
|
||||
.isNull(User::getRoleId)
|
||||
).stream()
|
||||
.map(User::getId)
|
||||
.collect(Collectors.toSet());
|
||||
// 获取默认角色id
|
||||
String defaultRoleId = getDefaultRoleId();
|
||||
List<User> updateUserParams = list.stream()
|
||||
// 过滤掉空部门id及长度超长的
|
||||
.filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH)
|
||||
.map(userDeptIds -> {
|
||||
User user = new User();
|
||||
user.setId(userDeptIds.getUserId());
|
||||
user.setDeptId(userDeptIds.getDeptIds());
|
||||
user.setDeptCodes(userDeptIds.getDeptCodes());
|
||||
if (noRoleUserIds.contains(userDeptIds.getUserId())) {
|
||||
user.setRoleId(defaultRoleId);
|
||||
}
|
||||
return user;
|
||||
}).toList();
|
||||
userService.updateBatchById(updateUserParams);
|
||||
OAResponseData<OADepartmentResponse> responseData = oaResponse.getData();
|
||||
List<OADepartmentResponse> oaDepartments = responseData.getDataList() == null
|
||||
? Collections.emptyList() : responseData.getDataList();
|
||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||
OrgSyncCount syncCount = handleDept(oaDepartments, true);
|
||||
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
||||
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
|
||||
}
|
||||
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
|
||||
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
||||
// 部门分批同步完成后,重建层级、清理无法挂载数据并刷新祖级列表
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
deptService.updateAncestors(null);
|
||||
}
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 {}/{},成功{},跳过{}",
|
||||
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||
return pageVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装组织分页同步结果
|
||||
*/
|
||||
private OaOrgSyncPageVO buildOrgSyncPageVO(String stage, int pageNo, int pageSize, long totalSize,
|
||||
int fetchedCount, OrgSyncCount syncCount) {
|
||||
OaOrgSyncPageVO pageVO = new OaOrgSyncPageVO();
|
||||
pageVO.setStage(stage);
|
||||
pageVO.setCurrent(pageNo);
|
||||
pageVO.setSize(pageSize);
|
||||
pageVO.setTotal(totalSize);
|
||||
pageVO.setFetchedCount(fetchedCount);
|
||||
pageVO.setSyncedCount(syncCount.getSyncedCount());
|
||||
pageVO.setSkippedCount(syncCount.getSkippedCount());
|
||||
boolean finished = fetchedCount == 0
|
||||
|| fetchedCount < pageSize
|
||||
|| (long) pageNo * pageSize >= totalSize;
|
||||
pageVO.setFinished(finished);
|
||||
return pageVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 OA 公司分页查询参数
|
||||
*
|
||||
* @param startTime 增量查询开始时间
|
||||
* @return 查询参数
|
||||
*/
|
||||
private OACompanySearch buildCompanySearch(Date startTime) {
|
||||
OACompanySearch companySearch = new OACompanySearch();
|
||||
companySearch.setCurPage(1);
|
||||
companySearch.setPageSize(20);
|
||||
if (startTime != null) {
|
||||
companySearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
}
|
||||
return companySearch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 OA 部门分页查询参数
|
||||
*
|
||||
* @param startTime 增量查询开始时间
|
||||
* @param subCompanyIds 子公司 id 列表
|
||||
* @return 查询参数
|
||||
*/
|
||||
private OADepartmentSearch buildDepartmentSearch(Date startTime, String subCompanyIds) {
|
||||
OADepartmentSearch departmentSearch = new OADepartmentSearch();
|
||||
departmentSearch.setCurPage(1);
|
||||
departmentSearch.setPageSize(20);
|
||||
departmentSearch.setSubcompanyid1(subCompanyIds);
|
||||
if (startTime != null) {
|
||||
departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
}
|
||||
return departmentSearch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 OA 人员分页查询参数
|
||||
*
|
||||
* @param startTime 增量查询开始时间
|
||||
* @return 查询参数
|
||||
*/
|
||||
private OAPersonSearch buildPersonSearch(Date startTime) {
|
||||
OAPersonSearch personSearch = new OAPersonSearch();
|
||||
personSearch.setCurPage(1);
|
||||
personSearch.setPageSize(20);
|
||||
personSearch.setCreated("");
|
||||
personSearch.setWorkcode("");
|
||||
personSearch.setSubcompanyid1("");
|
||||
personSearch.setDepartmentid("");
|
||||
personSearch.setJobtitleid("");
|
||||
personSearch.setId("");
|
||||
personSearch.setLoginid("");
|
||||
personSearch.setIsadaccount("");
|
||||
personSearch.setModified(startTime == null ? "" : DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
return personSearch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理oa公司
|
||||
* @param oaCompanies
|
||||
* @return 未处理的数据
|
||||
* @param oaCompanies OA公司列表
|
||||
* @param syncAll true=自动同步时不过滤字典白名单;false=仅同步配置的公司名称
|
||||
* @return 同步统计
|
||||
*/
|
||||
private List<Dept> handleCompany(List<OACompanyResponse> oaCompanies) {
|
||||
private OrgSyncCount handleCompany(List<OACompanyResponse> oaCompanies, boolean syncAll) {
|
||||
if (CollectionUtil.isEmpty(oaCompanies)) {
|
||||
// 数据为空,直接返回
|
||||
return Collections.emptyList();
|
||||
return OrgSyncCount.empty();
|
||||
}
|
||||
// 获取需要的公司名称
|
||||
Set<String> companyNames = getCompanyNames();
|
||||
Set<String> companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
|
||||
// 转换数据
|
||||
List<Dept> allParam = oaCompanies.stream()
|
||||
// 筛选只要需要的公司
|
||||
.filter(company -> companyNames.contains(company.getSubcompanyname()))
|
||||
// 强制跳过上级公司为 0 的数据
|
||||
.filter(company -> !OAConstant.ROOT_COMPANY_ID.equals(company.getSupsubcomid()))
|
||||
// 非全量同步时,筛选只要配置的公司
|
||||
.filter(company -> syncAll || companyNames.contains(company.getSubcompanyname()))
|
||||
.map(deptConvert::company2dept)
|
||||
.toList();
|
||||
int filteredSkipCount = oaCompanies.size() - allParam.size();
|
||||
if (CollectionUtil.isEmpty(allParam)) {
|
||||
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
|
||||
}
|
||||
|
||||
// 查询数据库的部门,转换成map
|
||||
Map<String, Long> deptMap = getAllCompanyDeptMap();
|
||||
|
||||
// 处理部门
|
||||
return handleDept(allParam, deptMap, DeptCategory.COMPANY);
|
||||
List<Dept> notHandledList = handleDept(allParam, deptMap, DeptCategory.COMPANY);
|
||||
int syncedCount = allParam.size() - notHandledList.size();
|
||||
int skippedCount = filteredSkipCount + notHandledList.size();
|
||||
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理oa部门
|
||||
* @param oaDepts
|
||||
* @return 未处理的数据
|
||||
* @param oaDepts OA部门列表
|
||||
* @param syncAll true=自动同步时不过滤根公司部门白名单;false=沿用原过滤规则
|
||||
* @return 同步统计
|
||||
*/
|
||||
private List<Dept> handleDept(List<OADepartmentResponse> oaDepts) {
|
||||
private OrgSyncCount handleDept(List<OADepartmentResponse> oaDepts, boolean syncAll) {
|
||||
if (CollectionUtil.isEmpty(oaDepts)) {
|
||||
// 数据为空,直接返回
|
||||
return Collections.emptyList();
|
||||
return OrgSyncCount.empty();
|
||||
}
|
||||
// 查询所有公司的编码和id的map
|
||||
Map<String, Long> companyDeptMap = getAllCompanyDeptMap();
|
||||
// 根公司id
|
||||
String rootCompanyId = getRootCompanyId();
|
||||
if (rootCompanyId == null) {
|
||||
return Collections.emptyList();
|
||||
if (!syncAll && rootCompanyId == null) {
|
||||
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
|
||||
}
|
||||
// 根公司下要同步的部门名称
|
||||
Set<String> rootCompanyDeptNames = getRootCompanyDeptNames();
|
||||
Set<String> rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
|
||||
// 转换数据
|
||||
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))
|
||||
.toList();
|
||||
int filteredSkipCount = oaDepts.size() - allParam.size();
|
||||
if (CollectionUtil.isEmpty(allParam)) {
|
||||
return Collections.emptyList();
|
||||
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
|
||||
}
|
||||
Set<String> deptCodes = allParam.stream()
|
||||
.map(Dept::getDeptCode)
|
||||
.collect(Collectors.toSet());
|
||||
// 查询数据库的部门,转换成map
|
||||
Map<String, Long> deptMap = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
Set<String> deptCodes = new HashSet<>();
|
||||
allParam.forEach(dept -> {
|
||||
if (StringUtils.isNotEmpty(dept.getDeptCode())) {
|
||||
deptCodes.add(dept.getDeptCode());
|
||||
}
|
||||
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())
|
||||
.in(Dept::getDeptCode, deptCodes)
|
||||
).stream()
|
||||
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
|
||||
.collect(Collectors.toMap(Dept::getDeptCode, Dept::getId, (a, b) -> b));
|
||||
// 公司编码也放入 map,根部门挂到公司时才能解析父级
|
||||
companyDeptMap.forEach(deptMap::putIfAbsent);
|
||||
// 处理部门
|
||||
return this.handleDept(allParam, deptMap, DeptCategory.DEPT);
|
||||
List<Dept> notHandledList = this.handleDept(allParam, deptMap, DeptCategory.DEPT);
|
||||
int syncedCount = allParam.size() - notHandledList.size();
|
||||
int skippedCount = filteredSkipCount + notHandledList.size();
|
||||
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -471,14 +594,30 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
});
|
||||
|
||||
// 重新把没有父id的数据设置一下父id
|
||||
allParam.stream()
|
||||
.filter(dept -> dept.getParentId() == null && deptMap.containsKey(dept.getDeptCode()))
|
||||
.forEach(dept -> dept.setParentId(deptMap.get(dept.getParentCode())));
|
||||
// 重新把没有父id的数据设置一下父id;上级尚未入库时,新增记录先挂到根节点,全部分批完成后重建层级并清理无法挂载的数据
|
||||
int unresolvedParentCount = 0;
|
||||
for (Dept dept : allParam) {
|
||||
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()
|
||||
.filter(dept -> dept.getParentId() != null)
|
||||
.filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
|
||||
.toList();
|
||||
// 不存在的新增
|
||||
List<Dept> addList = list.stream()
|
||||
@@ -496,31 +635,86 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
|
||||
deptService.updateBatchById(updateList);
|
||||
}
|
||||
// 返回未处理的
|
||||
// 返回仍未保存的数据
|
||||
return allParam.stream()
|
||||
.filter(dept -> dept.getParentId() == null)
|
||||
.filter(dept -> dept.getParentId() == null && !existsDeptIds.contains(dept.getId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取oa查询参数,子公司id参数
|
||||
* @return
|
||||
* 组织同步统计
|
||||
*/
|
||||
private String getSubCompanyIds() {
|
||||
Set<String> companyNames = this.getCompanyNames();
|
||||
if (companyNames.isEmpty()) {
|
||||
private static class OrgSyncCount {
|
||||
private final int syncedCount;
|
||||
private final int skippedCount;
|
||||
private final List<Dept> notHandledList;
|
||||
|
||||
private OrgSyncCount(int syncedCount, int skippedCount, List<Dept> notHandledList) {
|
||||
this.syncedCount = syncedCount;
|
||||
this.skippedCount = skippedCount;
|
||||
this.notHandledList = notHandledList == null ? Collections.emptyList() : notHandledList;
|
||||
}
|
||||
|
||||
private static OrgSyncCount empty() {
|
||||
return new OrgSyncCount(0, 0, Collections.emptyList());
|
||||
}
|
||||
|
||||
private int getSyncedCount() {
|
||||
return syncedCount;
|
||||
}
|
||||
|
||||
private int getSkippedCount() {
|
||||
return skippedCount;
|
||||
}
|
||||
|
||||
private List<Dept> getNotHandledList() {
|
||||
return notHandledList;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OA 部门查询使用的子公司 id
|
||||
*
|
||||
* @param syncAll true=取已同步的全部 OA 公司;false=仅取字典配置的公司
|
||||
* @return 逗号分隔的 OA 公司 id
|
||||
*/
|
||||
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();
|
||||
if (companyNames.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.in(Dept::getDeptName, companyNames)
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
);
|
||||
}
|
||||
return companyList.stream()
|
||||
.map(this::resolveOaCompanyId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析本地公司对应的 OA 公司 id
|
||||
*/
|
||||
private String resolveOaCompanyId(Dept dept) {
|
||||
if (dept == null) {
|
||||
return null;
|
||||
}
|
||||
return deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
// 根据名称查询公司
|
||||
.in(Dept::getDeptName, companyNames)
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
).stream()
|
||||
// 只要部门编码不为空,且包含了oa公司前缀的
|
||||
.filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().contains(OAConvertConstant.COMPANY_OA_PREFIX))
|
||||
// 去掉oa公司前缀
|
||||
.map(dept -> dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, ""))
|
||||
.collect(Collectors.joining(","));
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -634,16 +828,4 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
// 部门编码去掉前缀,就是oa的id
|
||||
return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户部门唯一标识,用户id+公司编码+部门编码
|
||||
* @param userDept
|
||||
* @return
|
||||
*/
|
||||
private String getUserDeptKey(UserDept userDept) {
|
||||
if (userDept == null) {
|
||||
return null;
|
||||
}
|
||||
return userDept.getUserId() + userDept.getCompanyCode() + userDept.getDeptCode();
|
||||
}
|
||||
}
|
||||
|
||||
+609
@@ -0,0 +1,609 @@
|
||||
/**
|
||||
* 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.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springblade.common.constant.DataStatusEnum;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.constant.BladeConstant;
|
||||
import org.springblade.core.tool.utils.DigestUtil;
|
||||
import org.springblade.system.cache.ParamCache;
|
||||
import org.springblade.system.convert.UserConvert;
|
||||
import org.springblade.system.log.ComposeLogUtil;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.entity.Role;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.system.pojo.entity.UserDept;
|
||||
import org.springblade.system.pojo.enums.DeptCategory;
|
||||
import org.springblade.system.pojo.vo.UserDeptIdsVO;
|
||||
import org.springblade.system.service.IDeptService;
|
||||
import org.springblade.system.service.IRoleService;
|
||||
import org.springblade.system.service.IUserDeptService;
|
||||
import org.springblade.system.service.IUserService;
|
||||
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_PASSWORD;
|
||||
import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_ROLE;
|
||||
import static org.springblade.common.constant.CommonConstant.DEFAULT_ROLE;
|
||||
import static org.springblade.common.constant.CommonConstant.YES;
|
||||
|
||||
/**
|
||||
* 从 OA 人员列表提取组织并同步人员
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OaUserListSyncHelper {
|
||||
|
||||
private static final int MAX_DEPT_ID_LENGTH = 2000;
|
||||
|
||||
private final IDeptService deptService;
|
||||
private final IUserService userService;
|
||||
private final UserConvert userConvert;
|
||||
private final IUserDeptService userDeptService;
|
||||
private final IRoleService roleService;
|
||||
|
||||
/**
|
||||
* 从人员数据提取二级公司、三级部门,并挂到「桂物物流集团」下
|
||||
*
|
||||
* @param oaPersons OA人员
|
||||
* @return 组织索引
|
||||
*/
|
||||
public OaOrgIndex syncOrgsFromPersons(List<OAPersonResponse> oaPersons) {
|
||||
OaOrgIndex orgIndex = new OaOrgIndex();
|
||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||
return orgIndex;
|
||||
}
|
||||
Date orgSyncStart = new Date();
|
||||
Dept rootCompany = this.getOrCreateRootCompany();
|
||||
String tenantId = resolveTenantId();
|
||||
Map<String, OAPersonResponse> companyPersonMap = new LinkedHashMap<>();
|
||||
Map<String, OAPersonResponse> departmentPersonMap = new LinkedHashMap<>();
|
||||
for (OAPersonResponse oaPerson : oaPersons) {
|
||||
String companyKey = resolveCompanyKey(oaPerson);
|
||||
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||
companyPersonMap.putIfAbsent(companyKey, oaPerson);
|
||||
}
|
||||
String departmentKey = resolveDepartmentKey(oaPerson);
|
||||
if (StringUtils.isNotBlank(departmentKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())
|
||||
&& StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||
departmentPersonMap.putIfAbsent(departmentKey, oaPerson);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Dept> existingCompanyByCode = new HashMap<>();
|
||||
Map<String, Dept> existingCompanyByName = new HashMap<>();
|
||||
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getParentId, rootCompany.getId())
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
).forEach(dept -> {
|
||||
if (StringUtils.isNotBlank(dept.getDeptCode())) {
|
||||
existingCompanyByCode.put(dept.getDeptCode(), dept);
|
||||
}
|
||||
if (StringUtils.isNotBlank(dept.getDeptName())) {
|
||||
existingCompanyByName.put(dept.getDeptName(), dept);
|
||||
}
|
||||
});
|
||||
List<Dept> addCompanies = new ArrayList<>();
|
||||
List<Dept> updateCompanies = new ArrayList<>();
|
||||
companyPersonMap.forEach((companyKey, oaPerson) -> {
|
||||
String oaCode = buildCompanyCode(oaPerson, companyKey);
|
||||
Dept existing = existingCompanyByCode.get(oaCode);
|
||||
if (existing == null) {
|
||||
existing = existingCompanyByName.get(oaPerson.getSubcompanyname());
|
||||
}
|
||||
Dept company = this.upsertOrg(oaPerson.getSubcompanyname(), oaCode, existing, rootCompany, tenantId,
|
||||
DeptCategory.COMPANY, addCompanies, updateCompanies);
|
||||
orgIndex.companyByOaId.put(companyKey, company);
|
||||
});
|
||||
this.saveOrgs(addCompanies, updateCompanies, DeptCategory.COMPANY);
|
||||
|
||||
Map<String, Dept> existingDeptByCode = new HashMap<>();
|
||||
Map<String, Dept> existingDeptByParentAndName = new HashMap<>();
|
||||
List<Long> companyIds = orgIndex.companyByOaId.values().stream()
|
||||
.map(Dept::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(companyIds)) {
|
||||
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.in(Dept::getParentId, companyIds)
|
||||
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||
).forEach(dept -> {
|
||||
if (StringUtils.isNotBlank(dept.getDeptCode())) {
|
||||
existingDeptByCode.put(dept.getDeptCode(), dept);
|
||||
}
|
||||
if (dept.getParentId() != null && StringUtils.isNotBlank(dept.getDeptName())) {
|
||||
existingDeptByParentAndName.put(dept.getParentId() + "#" + dept.getDeptName(), dept);
|
||||
}
|
||||
});
|
||||
}
|
||||
List<Dept> addDepartments = new ArrayList<>();
|
||||
List<Dept> updateDepartments = new ArrayList<>();
|
||||
departmentPersonMap.forEach((departmentKey, oaPerson) -> {
|
||||
Dept parentCompany = orgIndex.findCompany(oaPerson);
|
||||
if (parentCompany == null || parentCompany.getId() == null) {
|
||||
return;
|
||||
}
|
||||
String oaCode = buildDepartmentCode(oaPerson, departmentKey);
|
||||
Dept existing = existingDeptByCode.get(oaCode);
|
||||
if (existing == null) {
|
||||
existing = existingDeptByParentAndName.get(parentCompany.getId() + "#" + oaPerson.getDepartmentname());
|
||||
}
|
||||
Dept department = this.upsertOrg(oaPerson.getDepartmentname(), oaCode, existing, parentCompany, tenantId,
|
||||
DeptCategory.DEPT, addDepartments, updateDepartments);
|
||||
orgIndex.deptByOaId.put(departmentKey, department);
|
||||
orgIndex.deptByCompanyAndName.put(resolveCompanyKey(oaPerson) + "#" + oaPerson.getDepartmentname(), department);
|
||||
});
|
||||
this.saveOrgs(addDepartments, updateDepartments, DeptCategory.DEPT);
|
||||
deptService.updateAncestors(orgSyncStart);
|
||||
ComposeLogUtil.getLastLog().info("同步人员提取组织完成,二级公司{}个,三级部门{}个",
|
||||
orgIndex.companyByOaId.size(), orgIndex.deptByOaId.size());
|
||||
return orgIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步人员并绑定到三级部门
|
||||
*
|
||||
* @param oaPersons OA人员
|
||||
* @param orgIndex 组织索引
|
||||
* @return 本批同步成功与跳过数量
|
||||
*/
|
||||
public PersonSyncCount handlePerson(List<OAPersonResponse> oaPersons, OaOrgIndex orgIndex) {
|
||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||
return new PersonSyncCount(0, 0);
|
||||
}
|
||||
Map<String, OAPersonResponse> uniquePersonMap = new LinkedHashMap<>();
|
||||
int skippedCount = 0;
|
||||
for (OAPersonResponse oaPerson : oaPersons) {
|
||||
String account = userConvert.resolveAccount(oaPerson);
|
||||
if (StringUtils.isBlank(account)) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
uniquePersonMap.putIfAbsent(account, oaPerson);
|
||||
}
|
||||
if (uniquePersonMap.isEmpty()) {
|
||||
ComposeLogUtil.getLastLog().warn("OA人员均缺少loginid/工号/手机号,跳过人员同步");
|
||||
return new PersonSyncCount(0, skippedCount);
|
||||
}
|
||||
String tenantId = resolveTenantId();
|
||||
String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
||||
List<User> users = uniquePersonMap.values().stream()
|
||||
.map(person -> {
|
||||
User user = userConvert.person2user(person, defaultPassword);
|
||||
user.setTenantId(tenantId);
|
||||
return user;
|
||||
})
|
||||
.toList();
|
||||
List<String> accounts = users.stream()
|
||||
.map(User::getAccount)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
List<String> phones = users.stream()
|
||||
.map(User::getPhone)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<String, User> existingByAccount = new HashMap<>();
|
||||
Map<String, User> existingByPhone = new HashMap<>();
|
||||
userService.list(Wrappers.<User>lambdaQuery()
|
||||
.and(wrapper -> {
|
||||
wrapper.in(User::getAccount, accounts);
|
||||
if (CollectionUtil.isNotEmpty(phones)) {
|
||||
wrapper.or().in(User::getPhone, phones);
|
||||
}
|
||||
})
|
||||
).stream()
|
||||
.peek(userService::decryptPhone)
|
||||
.forEach(user -> {
|
||||
if (StringUtils.isNotBlank(user.getAccount())) {
|
||||
existingByAccount.put(user.getAccount(), user);
|
||||
}
|
||||
if (StringUtils.isNotBlank(user.getPhone())) {
|
||||
existingByPhone.put(user.getPhone(), user);
|
||||
}
|
||||
});
|
||||
Map<String, Long> userMap = new HashMap<>();
|
||||
Set<Long> existsUserIds = new HashSet<>();
|
||||
users.forEach(user -> {
|
||||
User existingUser = existingByAccount.get(user.getAccount());
|
||||
if (existingUser == null && StringUtils.isNotBlank(user.getPhone())) {
|
||||
existingUser = existingByPhone.get(user.getPhone());
|
||||
}
|
||||
if (existingUser != null) {
|
||||
user.setId(existingUser.getId());
|
||||
user.setPassword(null);
|
||||
existsUserIds.add(existingUser.getId());
|
||||
} else {
|
||||
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
user.setPostId("-1");
|
||||
user.setPersonCategory(1);
|
||||
user.setDataScopeRange(1);
|
||||
user.setDataLevelRange(1);
|
||||
user.setIncludeNewCustomer(0);
|
||||
userService.encryptPhone(user);
|
||||
}
|
||||
userMap.put(user.getAccount(), user.getId());
|
||||
});
|
||||
|
||||
List<User> addUsers = users.stream()
|
||||
.filter(user -> !existsUserIds.contains(user.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(addUsers)) {
|
||||
ComposeLogUtil.getLastLog().info("批量新增用户:{}", addUsers.size());
|
||||
userService.saveBatch(addUsers);
|
||||
}
|
||||
List<User> updateUsers = users.stream()
|
||||
.filter(user -> existsUserIds.contains(user.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(updateUsers)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
|
||||
userService.updateBatchById(updateUsers);
|
||||
}
|
||||
ComposeLogUtil.getLastLog().info("缺少账号已跳过的人员:{}", skippedCount);
|
||||
if (userMap.isEmpty()) {
|
||||
return new PersonSyncCount(0, skippedCount);
|
||||
}
|
||||
|
||||
Map<String, Long> userDeptMap = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||
.in(UserDept::getUserId, userMap.values())
|
||||
).stream()
|
||||
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second));
|
||||
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
|
||||
List<UserDept> userDeptList = oaPersons.stream()
|
||||
.filter(oaPerson -> userMap.containsKey(userConvert.resolveAccount(oaPerson)))
|
||||
.map(oaPerson -> this.buildUserDept(oaPerson, userMap, orgIndex))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
userDeptList.forEach(userDept -> {
|
||||
String userDeptKey = getUserDeptKey(userDept);
|
||||
if (userDeptMap.containsKey(userDeptKey)) {
|
||||
userDept.setId(userDeptMap.get(userDeptKey));
|
||||
} else {
|
||||
userDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
}
|
||||
});
|
||||
List<UserDept> addList = userDeptList.stream()
|
||||
.filter(userDept -> !existsUserDeptIds.contains(userDept.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(addList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量新增用户部门:{}", addList.size());
|
||||
userDeptService.saveBatch(addList);
|
||||
}
|
||||
List<UserDept> updateList = userDeptList.stream()
|
||||
.filter(userDept -> existsUserDeptIds.contains(userDept.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改用户部门:{}", updateList.size());
|
||||
userDeptService.updateBatchById(updateList);
|
||||
}
|
||||
Collection<Long> userIds = userMap.values();
|
||||
List<UserDeptIdsVO> list = userDeptService.queryUserDeptIds(userIds);
|
||||
Set<Long> noRoleUserIds = userService.list(Wrappers.<User>lambdaQuery()
|
||||
.in(User::getId, userIds)
|
||||
.and(wrapper -> wrapper.isNull(User::getRoleId)
|
||||
.or().eq(User::getRoleId, "")
|
||||
.or().eq(User::getRoleId, "-1"))
|
||||
).stream()
|
||||
.map(User::getId)
|
||||
.collect(Collectors.toSet());
|
||||
String defaultRoleId = getDefaultRoleId();
|
||||
List<User> updateUserParams = list.stream()
|
||||
.filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH)
|
||||
.map(userDeptIds -> {
|
||||
User user = new User();
|
||||
user.setId(userDeptIds.getUserId());
|
||||
user.setDeptId(userDeptIds.getDeptIds());
|
||||
user.setDeptCodes(userDeptIds.getDeptCodes());
|
||||
if (noRoleUserIds.contains(userDeptIds.getUserId())) {
|
||||
user.setRoleId(defaultRoleId);
|
||||
}
|
||||
return user;
|
||||
}).toList();
|
||||
if (CollectionUtil.isNotEmpty(updateUserParams)) {
|
||||
userService.updateBatchById(updateUserParams);
|
||||
}
|
||||
return new PersonSyncCount(users.size(), skippedCount);
|
||||
}
|
||||
|
||||
private Dept getOrCreateRootCompany() {
|
||||
Dept rootCompany = deptService.getOne(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptName, OAConvertConstant.ROOT_COMPANY_NAME)
|
||||
.last("limit 1"), false);
|
||||
if (rootCompany != null) {
|
||||
return rootCompany;
|
||||
}
|
||||
rootCompany = new Dept();
|
||||
rootCompany.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
rootCompany.setTenantId(resolveTenantId());
|
||||
rootCompany.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||
rootCompany.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootCompany.setDeptName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
rootCompany.setFullName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
rootCompany.setShortName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
rootCompany.setDeptCode("OACROOT");
|
||||
rootCompany.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootCompany.setBelongCompanyCode("OACROOT");
|
||||
rootCompany.setDeptCategory(DeptCategory.COMPANY.getCode());
|
||||
rootCompany.setSort(0);
|
||||
rootCompany.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||
rootCompany.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
rootCompany.setIsOa(YES);
|
||||
rootCompany.setIsPlatformCompany(0);
|
||||
rootCompany.setSyncTime(new Date());
|
||||
deptService.save(rootCompany);
|
||||
ComposeLogUtil.getLastLog().info("已创建顶级组织:{}", OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
return rootCompany;
|
||||
}
|
||||
|
||||
private Dept upsertOrg(String name, String oaCode, Dept existing, Dept parent, String tenantId,
|
||||
DeptCategory deptCategory, List<Dept> addList, List<Dept> updateList) {
|
||||
if (existing != null) {
|
||||
Dept updateParam = new Dept();
|
||||
updateParam.setId(existing.getId());
|
||||
updateParam.setDeptName(name);
|
||||
updateParam.setFullName(name);
|
||||
updateParam.setShortName(name);
|
||||
updateParam.setParentId(parent.getId());
|
||||
updateParam.setParentCode(parent.getDeptCode());
|
||||
updateParam.setAncestors(buildAncestors(parent));
|
||||
updateParam.setIsOa(YES);
|
||||
updateParam.setSyncTime(new Date());
|
||||
updateList.add(updateParam);
|
||||
existing.setDeptName(name);
|
||||
existing.setFullName(name);
|
||||
existing.setShortName(name);
|
||||
existing.setParentId(parent.getId());
|
||||
existing.setParentCode(parent.getDeptCode());
|
||||
existing.setAncestors(updateParam.getAncestors());
|
||||
return existing;
|
||||
}
|
||||
Dept dept = this.buildOrgDept(name, oaCode, parent, tenantId, deptCategory);
|
||||
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
addList.add(dept);
|
||||
return dept;
|
||||
}
|
||||
|
||||
private void saveOrgs(List<Dept> addList, List<Dept> updateList, DeptCategory deptCategory) {
|
||||
if (CollectionUtil.isNotEmpty(addList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size());
|
||||
deptService.saveBatch(addList);
|
||||
}
|
||||
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
|
||||
deptService.updateBatchById(updateList);
|
||||
}
|
||||
}
|
||||
|
||||
private Dept buildOrgDept(String name, String deptCode, Dept parent, String tenantId, DeptCategory deptCategory) {
|
||||
Dept dept = new Dept();
|
||||
dept.setTenantId(tenantId);
|
||||
dept.setParentId(parent.getId());
|
||||
dept.setParentCode(parent.getDeptCode());
|
||||
dept.setAncestors(this.buildAncestors(parent));
|
||||
dept.setDeptName(name);
|
||||
dept.setFullName(name);
|
||||
dept.setShortName(name);
|
||||
dept.setDeptCode(deptCode);
|
||||
dept.setBelongCompanyCode(DeptCategory.COMPANY.equals(deptCategory) ? deptCode : parent.getBelongCompanyCode());
|
||||
dept.setDeptCategory(deptCategory.getCode());
|
||||
dept.setSort(0);
|
||||
dept.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
dept.setIsOa(YES);
|
||||
dept.setIsPlatformCompany(0);
|
||||
dept.setSyncTime(new Date());
|
||||
return dept;
|
||||
}
|
||||
|
||||
private UserDept buildUserDept(OAPersonResponse oaPerson, Map<String, Long> userMap, OaOrgIndex orgIndex) {
|
||||
Dept department = orgIndex.findDept(oaPerson);
|
||||
if (department == null || department.getId() == null) {
|
||||
return null;
|
||||
}
|
||||
UserDept userDept = userConvert.person2userDept(oaPerson, userMap);
|
||||
if (userDept.getUserId() == null) {
|
||||
return null;
|
||||
}
|
||||
Dept company = orgIndex.findCompany(oaPerson);
|
||||
userDept.setDeptId(department.getId());
|
||||
userDept.setDeptCode(department.getDeptCode());
|
||||
userDept.setDeptName(department.getDeptName());
|
||||
if (company != null) {
|
||||
userDept.setCompanyCode(company.getDeptCode());
|
||||
userDept.setCompanyName(company.getDeptName());
|
||||
}
|
||||
return userDept;
|
||||
}
|
||||
|
||||
private String getDefaultRoleId() {
|
||||
String defaultRole = ParamCache.getValue(DEFAULT_PARAM_ROLE);
|
||||
if (defaultRole == null) {
|
||||
defaultRole = DEFAULT_ROLE;
|
||||
}
|
||||
List<Role> roleList = roleService.list(Wrappers.<Role>lambdaQuery()
|
||||
.eq(Role::getRoleAlias, defaultRole)
|
||||
);
|
||||
if (CollectionUtil.isEmpty(roleList)) {
|
||||
return null;
|
||||
}
|
||||
return roleList.get(0).getId().toString();
|
||||
}
|
||||
|
||||
private String resolveCompanyKey(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||
return oaPerson.getSubcompanyid1().trim();
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||
return "NAME:" + oaPerson.getSubcompanyname().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveDepartmentKey(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||
return oaPerson.getDepartmentid().trim();
|
||||
}
|
||||
String companyKey = resolveCompanyKey(oaPerson);
|
||||
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())) {
|
||||
return companyKey + ":" + oaPerson.getDepartmentname().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String buildCompanyCode(OAPersonResponse oaPerson, String companyKey) {
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||
return OAConvertConstant.COMPANY_OA_PREFIX + oaPerson.getSubcompanyid1().trim();
|
||||
}
|
||||
return OAConvertConstant.COMPANY_OA_PREFIX + "N" + Math.abs(companyKey.hashCode());
|
||||
}
|
||||
|
||||
private String buildDepartmentCode(OAPersonResponse oaPerson, String departmentKey) {
|
||||
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||
return OAConvertConstant.DEPARTMENT_OA_PREFIX + oaPerson.getDepartmentid().trim();
|
||||
}
|
||||
return OAConvertConstant.DEPARTMENT_OA_PREFIX + "N" + Math.abs(departmentKey.hashCode());
|
||||
}
|
||||
|
||||
private String buildAncestors(Dept parent) {
|
||||
String ancestors = parent.getAncestors();
|
||||
if (StringUtils.isBlank(ancestors)) {
|
||||
ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID);
|
||||
}
|
||||
return ancestors + "," + parent.getId();
|
||||
}
|
||||
|
||||
private String resolveTenantId() {
|
||||
String tenantId = AuthUtil.getTenantId();
|
||||
if (StringUtils.isBlank(tenantId)) {
|
||||
return BladeConstant.ADMIN_TENANT_ID;
|
||||
}
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
private String getUserDeptKey(UserDept userDept) {
|
||||
if (userDept == null) {
|
||||
return null;
|
||||
}
|
||||
return userDept.getUserId() + userDept.getCompanyCode() + userDept.getDeptCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 本批人员同步计数
|
||||
*/
|
||||
public static class PersonSyncCount {
|
||||
private final int syncedCount;
|
||||
private final int skippedCount;
|
||||
|
||||
public PersonSyncCount(int syncedCount, int skippedCount) {
|
||||
this.syncedCount = syncedCount;
|
||||
this.skippedCount = skippedCount;
|
||||
}
|
||||
|
||||
public int getSyncedCount() {
|
||||
return syncedCount;
|
||||
}
|
||||
|
||||
public int getSkippedCount() {
|
||||
return skippedCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OA 组织索引
|
||||
*/
|
||||
public static class OaOrgIndex {
|
||||
private final Map<String, Dept> companyByOaId = new HashMap<>();
|
||||
private final Map<String, Dept> deptByOaId = new HashMap<>();
|
||||
private final Map<String, Dept> deptByCompanyAndName = new HashMap<>();
|
||||
|
||||
private Dept findCompany(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||
Dept company = companyByOaId.get(oaPerson.getSubcompanyid1().trim());
|
||||
if (company != null) {
|
||||
return company;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||
return companyByOaId.get("NAME:" + oaPerson.getSubcompanyname().trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Dept findDept(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||
Dept department = deptByOaId.get(oaPerson.getDepartmentid().trim());
|
||||
if (department != null) {
|
||||
return department;
|
||||
}
|
||||
}
|
||||
String companyKey = StringUtils.isNotBlank(oaPerson.getSubcompanyid1())
|
||||
? oaPerson.getSubcompanyid1().trim()
|
||||
: (StringUtils.isNotBlank(oaPerson.getSubcompanyname()) ? "NAME:" + oaPerson.getSubcompanyname().trim() : null);
|
||||
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())) {
|
||||
Dept department = deptByCompanyAndName.get(companyKey + "#" + oaPerson.getDepartmentname());
|
||||
if (department != null) {
|
||||
return department;
|
||||
}
|
||||
return deptByOaId.get(companyKey + ":" + oaPerson.getDepartmentname().trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+640
-694
File diff suppressed because it is too large
Load Diff
+212
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.cache.utils.CacheUtil;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.redis.cache.BladeRedis;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.resource.feign.ISmsClient;
|
||||
import org.springblade.resource.utils.SmsUtil;
|
||||
import org.springblade.system.pojo.dto.PhoneChangeDTO;
|
||||
import org.springblade.system.pojo.dto.PhoneVerifyDTO;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.system.service.IUserPhoneService;
|
||||
import org.springblade.system.service.IUserService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
|
||||
|
||||
/**
|
||||
* 用户手机号变更服务实现
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserPhoneServiceImpl implements IUserPhoneService {
|
||||
|
||||
/**
|
||||
* 与登录短信一致,对应后台 /resource/sms 的 smsCode
|
||||
*/
|
||||
private static final String SMS_RESOURCE_CODE = "ali_reg";
|
||||
|
||||
/**
|
||||
* 原手机号已校验凭证(Redis)
|
||||
*/
|
||||
private static final String PHONE_CHANGE_VERIFIED_KEY = "blade:user:phone:change:verified:";
|
||||
|
||||
private static final Duration PHONE_CHANGE_VERIFIED_TTL = Duration.ofMinutes(15);
|
||||
|
||||
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
|
||||
|
||||
private final IUserService userService;
|
||||
private final ISmsClient smsClient;
|
||||
private final BladeRedis bladeRedis;
|
||||
|
||||
@Override
|
||||
public R sendCode(String phone) {
|
||||
String normalizedPhone = normalizePhone(phone);
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (Func.isEmpty(userId)) {
|
||||
throw new ServiceException("请先登录");
|
||||
}
|
||||
User currentUser = requireCurrentUser(userId);
|
||||
String tenantId = Func.toStr(currentUser.getTenantId(), AuthUtil.getTenantId());
|
||||
boolean isCurrentPhone = StringUtil.equals(normalizedPhone, Func.toStr(currentUser.getPhone()));
|
||||
if (!isCurrentPhone) {
|
||||
assertPhoneAvailable(tenantId, normalizedPhone, userId);
|
||||
}
|
||||
R result = smsClient.sendValidate(tenantId, SMS_RESOURCE_CODE, normalizedPhone);
|
||||
if (result == null || !result.isSuccess()) {
|
||||
return R.fail(SmsUtil.SEND_FAIL);
|
||||
}
|
||||
return R.data(result.getData(), SmsUtil.SEND_SUCCESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyOldPhone(PhoneVerifyDTO phoneVerify) {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (Func.isEmpty(userId)) {
|
||||
throw new ServiceException("请先登录");
|
||||
}
|
||||
String id = Func.toStr(phoneVerify.getId()).trim();
|
||||
String code = Func.toStr(phoneVerify.getCode()).trim();
|
||||
if (StringUtil.isBlank(id) || StringUtil.isBlank(code)) {
|
||||
throw new ServiceException("请先获取并填写验证码");
|
||||
}
|
||||
User currentUser = requireCurrentUser(userId);
|
||||
String oldPhone = Func.toStr(currentUser.getPhone()).trim();
|
||||
if (StringUtil.isBlank(oldPhone)) {
|
||||
throw new ServiceException("当前账号未绑定手机号");
|
||||
}
|
||||
validateSms(currentUser.getTenantId(), id, code, oldPhone);
|
||||
bladeRedis.setEx(PHONE_CHANGE_VERIFIED_KEY + userId, "1", PHONE_CHANGE_VERIFIED_TTL);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean changePhone(PhoneChangeDTO phoneChange) {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (Func.isEmpty(userId)) {
|
||||
throw new ServiceException("请先登录");
|
||||
}
|
||||
String verified = Func.toStr(bladeRedis.get(PHONE_CHANGE_VERIFIED_KEY + userId));
|
||||
if (!StringUtil.equals(verified, "1")) {
|
||||
throw new ServiceException("请先完成原手机号验证");
|
||||
}
|
||||
String id = Func.toStr(phoneChange.getId()).trim();
|
||||
String code = Func.toStr(phoneChange.getCode()).trim();
|
||||
String newPhone = normalizePhone(phoneChange.getNewPhone());
|
||||
if (StringUtil.isBlank(id) || StringUtil.isBlank(code)) {
|
||||
throw new ServiceException("请先获取并填写验证码");
|
||||
}
|
||||
User currentUser = requireCurrentUser(userId);
|
||||
String oldPhone = Func.toStr(currentUser.getPhone()).trim();
|
||||
if (StringUtil.equals(newPhone, oldPhone)) {
|
||||
throw new ServiceException("新手机号不可与当前手机号相同");
|
||||
}
|
||||
String tenantId = Func.toStr(currentUser.getTenantId(), AuthUtil.getTenantId());
|
||||
assertPhoneAvailable(tenantId, newPhone, userId);
|
||||
validateSms(tenantId, id, code, newPhone);
|
||||
|
||||
User updateUser = new User();
|
||||
updateUser.setId(userId);
|
||||
updateUser.setPhone(newPhone);
|
||||
// 账号若等于原手机号,同步更新,保证短信登录可用
|
||||
if (StringUtil.isNotBlank(oldPhone) && StringUtil.equals(oldPhone, Func.toStr(currentUser.getAccount()))) {
|
||||
assertAccountAvailable(tenantId, newPhone, userId);
|
||||
updateUser.setAccount(newPhone);
|
||||
}
|
||||
boolean updated = userService.updateById(updateUser);
|
||||
if (!updated) {
|
||||
throw new ServiceException("手机号修改失败");
|
||||
}
|
||||
bladeRedis.del(PHONE_CHANGE_VERIFIED_KEY + userId);
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
return true;
|
||||
}
|
||||
|
||||
private User requireCurrentUser(Long userId) {
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw new ServiceException("用户不存在");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
private void validateSms(String tenantId, String id, String value, String phone) {
|
||||
R result = smsClient.validateMessage(tenantId, SMS_RESOURCE_CODE, id, value, phone);
|
||||
if (result == null || !result.isSuccess()) {
|
||||
throw new ServiceException(SmsUtil.VALIDATE_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertPhoneAvailable(String tenantId, String phone, Long excludeUserId) {
|
||||
Long phoneCount = userService.count(
|
||||
Wrappers.<User>lambdaQuery()
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getPhone, phone)
|
||||
.ne(User::getId, excludeUserId)
|
||||
);
|
||||
if (phoneCount != null && phoneCount > 0L) {
|
||||
throw new ServiceException(StringUtil.format("当前手机 [{}] 已存在!", phone));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertAccountAvailable(String tenantId, String account, Long excludeUserId) {
|
||||
Long accountCount = userService.count(
|
||||
Wrappers.<User>lambdaQuery()
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getAccount, account)
|
||||
.ne(User::getId, excludeUserId)
|
||||
);
|
||||
if (accountCount != null && accountCount > 0L) {
|
||||
throw new ServiceException(StringUtil.format("当前用户 [{}] 已存在!", account));
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizePhone(String phone) {
|
||||
String normalizedPhone = Func.toStr(phone).trim();
|
||||
if (!MOBILE_PATTERN.matcher(normalizedPhone).matches()) {
|
||||
throw new ServiceException("手机号格式不正确");
|
||||
}
|
||||
return normalizedPhone;
|
||||
}
|
||||
|
||||
}
|
||||
+276
-14
@@ -31,10 +31,15 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.common.constant.DataStatusEnum;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.springblade.common.constant.ParamConstant;
|
||||
import org.springblade.common.constant.TenantConstant;
|
||||
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;
|
||||
@@ -63,6 +68,7 @@ import org.springblade.system.pojo.entity.*;
|
||||
import org.springblade.system.pojo.enums.DictEnum;
|
||||
import org.springblade.system.pojo.enums.UserType;
|
||||
import org.springblade.system.pojo.vo.UserVO;
|
||||
import org.springblade.system.props.IamSyncProperties;
|
||||
import org.springblade.system.service.IRoleService;
|
||||
import org.springblade.system.service.IUserDeptService;
|
||||
import org.springblade.system.service.IUserOauthService;
|
||||
@@ -71,7 +77,11 @@ 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;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
@@ -79,6 +89,8 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.springblade.common.constant.ParamConstant.DEFAULT_PARAM_PASSWORD;
|
||||
import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
|
||||
@@ -91,11 +103,14 @@ import static org.springblade.core.tenant.TenantGuard.EntityType.USER;
|
||||
*/
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@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 = "默认部门";
|
||||
private static final String IAM_SYNC_TENANT_ID = "000000";
|
||||
private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
||||
|
||||
private final IUserDeptService userDeptService;
|
||||
private final UserDataScopeMapper userDataScopeMapper;
|
||||
@@ -105,6 +120,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
private final BladeTenantProperties tenantProperties;
|
||||
|
||||
private final OAuth2Properties properties;
|
||||
private final IamSyncProperties iamSyncProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -132,6 +149,163 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
return saveUser(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int syncIamAccounts() {
|
||||
int pageNumber = 1;
|
||||
int fetchedCount = 0;
|
||||
int syncedCount = 0;
|
||||
int totalCount = -1;
|
||||
int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50;
|
||||
while (true) {
|
||||
JsonNode dataNode = requestIamAccountPage(pageNumber, pageSize);
|
||||
JsonNode accountList = dataNode.path("list");
|
||||
if (!accountList.isArray() || accountList.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
if (dataNode.has("total")) {
|
||||
totalCount = dataNode.path("total").asInt(totalCount);
|
||||
}
|
||||
for (JsonNode accountNode : accountList) {
|
||||
if (syncIamAccount(accountNode)) {
|
||||
syncedCount++;
|
||||
}
|
||||
}
|
||||
fetchedCount += accountList.size();
|
||||
int responsePage = dataNode.path("page").asInt(pageNumber);
|
||||
int responseSize = dataNode.path("size").asInt(pageSize);
|
||||
if ((totalCount >= 0 && fetchedCount >= totalCount)
|
||||
|| accountList.size() < pageSize
|
||||
|| (totalCount >= 0 && responsePage * responseSize >= totalCount)) {
|
||||
break;
|
||||
}
|
||||
pageNumber = responsePage + 1;
|
||||
}
|
||||
log.info("IAM账号同步完成,tenantId={}, fetchedCount={}, syncedCount={}", IAM_SYNC_TENANT_ID, fetchedCount, syncedCount);
|
||||
return syncedCount;
|
||||
}
|
||||
|
||||
private JsonNode requestIamAccountPage(int pageNumber, int pageSize) {
|
||||
try {
|
||||
Map<String, String> requestBody = new LinkedHashMap<>();
|
||||
requestBody.put("size", String.valueOf(pageSize));
|
||||
requestBody.put("page", String.valueOf(pageNumber));
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(iamSyncProperties.getAccountListUrl()))
|
||||
.timeout(Duration.ofSeconds(20))
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization()))
|
||||
.header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization()))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new ServiceException(StringUtil.format("IAM账号接口调用失败,HTTP状态码:{}", response.statusCode()));
|
||||
}
|
||||
JsonNode responseNode = objectMapper.readTree(response.body());
|
||||
if (!"0".equals(responseNode.path("code").asText())) {
|
||||
throw new ServiceException(StringUtil.format("IAM账号接口调用失败:{}", responseNode.path("msg").asText()));
|
||||
}
|
||||
JsonNode dataNode = responseNode.path("data");
|
||||
if (!dataNode.isObject()) {
|
||||
throw new ServiceException("IAM账号接口返回数据格式错误");
|
||||
}
|
||||
return dataNode;
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("调用IAM账号接口被中断,page={}", pageNumber, exception);
|
||||
throw new ServiceException("调用IAM账号接口被中断");
|
||||
} catch (IOException | IllegalArgumentException exception) {
|
||||
log.error("调用IAM账号接口失败,page={}", pageNumber, exception);
|
||||
throw new ServiceException("调用IAM账号接口失败");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean syncIamAccount(JsonNode accountNode) {
|
||||
String account = readIamText(accountNode, "accountNo", "account_no", "app_account__account_no");
|
||||
if (StringUtil.isBlank(account)) {
|
||||
log.warn("IAM账号缺少accountNo,跳过同步");
|
||||
return false;
|
||||
}
|
||||
String name = readIamText(accountNode, "name", "app_account__name");
|
||||
if (StringUtil.isBlank(name)) {
|
||||
name = readIamText(accountNode, "accountName", "account_name", "app_account__account_name");
|
||||
}
|
||||
if (StringUtil.isBlank(name)) {
|
||||
name = account;
|
||||
}
|
||||
Integer status = readIamInt(accountNode, "status", "app_account__status") == 1
|
||||
? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode();
|
||||
User user = userByAccount(IAM_SYNC_TENANT_ID, account);
|
||||
if (user == null) {
|
||||
user = new User();
|
||||
user.setTenantId(IAM_SYNC_TENANT_ID);
|
||||
user.setAccount(account);
|
||||
user.setName(name);
|
||||
user.setRealName(name);
|
||||
user.setRoleId(resolveIamDefaultRoleId());
|
||||
user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
|
||||
user.setPostId(StringPool.MINUS_ONE);
|
||||
user.setUserType(UserType.WEB.getCategory());
|
||||
user.setStatus(status);
|
||||
user.setIsOa(1);
|
||||
user.setSyncTime(new Date());
|
||||
applyUserDefaults(user);
|
||||
return saveUser(user);
|
||||
}
|
||||
boolean changed = !Objects.equals(user.getName(), name) || !Objects.equals(user.getRealName(), name)
|
||||
|| !Objects.equals(user.getStatus(), status) || !Objects.equals(user.getIsOa(), 1);
|
||||
if (isMissingIamRole(user.getRoleId())) {
|
||||
user.setRoleId(resolveIamDefaultRoleId());
|
||||
changed = true;
|
||||
}
|
||||
if (isMissingIamAssignment(user.getDeptId())) {
|
||||
user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
return true;
|
||||
}
|
||||
user.setName(name);
|
||||
user.setRealName(name);
|
||||
user.setStatus(status);
|
||||
user.setIsOa(1);
|
||||
user.setSyncTime(new Date());
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
return updateById(user);
|
||||
}
|
||||
|
||||
private String readIamText(JsonNode node, String... fieldNames) {
|
||||
JsonNode valueNode = findIamNode(node, fieldNames);
|
||||
return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim();
|
||||
}
|
||||
|
||||
private int readIamInt(JsonNode node, String... fieldNames) {
|
||||
JsonNode valueNode = findIamNode(node, fieldNames);
|
||||
return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0);
|
||||
}
|
||||
|
||||
private JsonNode findIamNode(JsonNode node, String... fieldNames) {
|
||||
for (String fieldName : fieldNames) {
|
||||
JsonNode valueNode = node.get(fieldName);
|
||||
if (valueNode != null && !valueNode.isNull()) {
|
||||
return valueNode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String normalizeAuthorizationHeader(String value) {
|
||||
if (StringUtil.isBlank(value)) {
|
||||
return StringPool.EMPTY;
|
||||
}
|
||||
if (StringUtil.startsWithIgnoreCase(value, "Basic ") || StringUtil.startsWithIgnoreCase(value, "Bearer ")) {
|
||||
return value;
|
||||
}
|
||||
return "Basic " + value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateUser(User user) {
|
||||
@@ -352,6 +526,46 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) {
|
||||
if (Func.isBlank(tenantId) || Func.isEmpty(userId) || Func.isBlank(openid)) {
|
||||
throw new ServiceException("绑定微信 openid 参数不完整");
|
||||
}
|
||||
String source = "WECHAT_MINI";
|
||||
UserOauth byOpenId = userOauthService.getOne(Wrappers.<UserOauth>lambdaQuery()
|
||||
.eq(UserOauth::getTenantId, tenantId)
|
||||
.eq(UserOauth::getSource, source)
|
||||
.eq(UserOauth::getUuid, openid)
|
||||
.last("LIMIT 1"));
|
||||
if (byOpenId != null) {
|
||||
byOpenId.setUserId(userId);
|
||||
if (Func.isNotBlank(phone)) {
|
||||
byOpenId.setUsername(phone);
|
||||
}
|
||||
return userOauthService.updateById(byOpenId);
|
||||
}
|
||||
UserOauth byUser = userOauthService.getOne(Wrappers.<UserOauth>lambdaQuery()
|
||||
.eq(UserOauth::getTenantId, tenantId)
|
||||
.eq(UserOauth::getSource, source)
|
||||
.eq(UserOauth::getUserId, userId)
|
||||
.last("LIMIT 1"));
|
||||
if (byUser != null) {
|
||||
byUser.setUuid(openid);
|
||||
if (Func.isNotBlank(phone)) {
|
||||
byUser.setUsername(phone);
|
||||
}
|
||||
return userOauthService.updateById(byUser);
|
||||
}
|
||||
UserOauth oauth = new UserOauth();
|
||||
oauth.setTenantId(tenantId);
|
||||
oauth.setUserId(userId);
|
||||
oauth.setUuid(openid);
|
||||
oauth.setUsername(Func.toStr(phone, ""));
|
||||
oauth.setSource(source);
|
||||
return userOauthService.save(oauth);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean grant(String userIds, String roleIds) {
|
||||
@@ -371,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());
|
||||
@@ -387,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));
|
||||
@@ -455,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);
|
||||
}
|
||||
});
|
||||
@@ -472,6 +688,9 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
*/
|
||||
private User buildImportUser(UserExcel userExcel, String tenantId) {
|
||||
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.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1));
|
||||
user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY));
|
||||
@@ -563,8 +782,31 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
if (user.getUserType() == null) {
|
||||
user.setUserType(UserType.WEB.getCategory());
|
||||
}
|
||||
if (StringUtil.isBlank(user.getRoleId())) {
|
||||
user.setRoleId(StringPool.MINUS_ONE);
|
||||
String defaultRoleId = resolveIamDefaultRoleId();
|
||||
User existingUser = userByAccount(user.getTenantId(), user.getAccount());
|
||||
if (existingUser != null) {
|
||||
boolean changed = false;
|
||||
if (isMissingIamRole(existingUser.getRoleId())) {
|
||||
existingUser.setRoleId(defaultRoleId);
|
||||
changed = true;
|
||||
}
|
||||
if (isMissingIamAssignment(existingUser.getDeptId())) {
|
||||
existingUser.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
|
||||
changed = true;
|
||||
}
|
||||
if (existingUser.getIsOa() == null || existingUser.getIsOa() != 1) {
|
||||
existingUser.setIsOa(1);
|
||||
existingUser.setSyncTime(new Date());
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
return true;
|
||||
}
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
return this.updateById(existingUser);
|
||||
}
|
||||
if (isMissingIamRole(user.getRoleId())) {
|
||||
user.setRoleId(defaultRoleId);
|
||||
}
|
||||
if (StringUtil.isBlank(user.getDeptId())) {
|
||||
user.setDeptId(StringPool.MINUS_ONE);
|
||||
@@ -579,6 +821,26 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
return saveUser(user);
|
||||
}
|
||||
|
||||
private boolean isMissingIamRole(String roleId) {
|
||||
return StringUtil.isBlank(roleId) || StringPool.MINUS_ONE.equals(roleId);
|
||||
}
|
||||
|
||||
private String resolveIamDefaultRoleId() {
|
||||
return resolveIamDefaultId(IAM_DEFAULT_ROLE_NAME);
|
||||
}
|
||||
|
||||
private String resolveIamDefaultId(String dictValue) {
|
||||
String dictKey = DictCache.getKey(IAM_DEFAULT_DICT_CODE, dictValue);
|
||||
if (StringUtil.isBlank(dictKey) || StringPool.MINUS_ONE.equals(dictKey)) {
|
||||
throw new ServiceException(StringUtil.format("IAM默认配置 [{}] 未配置有效键值", dictValue));
|
||||
}
|
||||
return dictKey;
|
||||
}
|
||||
|
||||
private boolean isMissingIamAssignment(String value) {
|
||||
return StringUtil.isBlank(value) || StringPool.MINUS_ONE.equals(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updatePlatform(Long userId, Integer userType, String userExt) {
|
||||
@@ -801,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())) {
|
||||
@@ -884,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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# 本机多网卡时 Nacos 易误选 169.254.*(链路本地),导致网关 Host is down
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
ip: ${NACOS_DISCOVERY_IP:127.0.0.1}
|
||||
@@ -23,3 +23,21 @@ spring:
|
||||
url: ${blade.datasource.${spring.profiles.active}.url}
|
||||
username: ${blade.datasource.${spring.profiles.active}.username}
|
||||
password: ${blade.datasource.${spring.profiles.active}.password}
|
||||
|
||||
# IAM账号同步
|
||||
iam:
|
||||
sync:
|
||||
account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST}
|
||||
org-list-url: ${IAM_SSO_ORG_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ORG_LIST}
|
||||
authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}
|
||||
profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==}
|
||||
page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50}
|
||||
|
||||
# OA组织/人员同步走同一 gwzh 网关,仅需 Authorization(与可用 curl 一致)
|
||||
thirdParty:
|
||||
oa:
|
||||
baseUrl: ${OA_BASE_URL:http://172.16.204.83:38000}
|
||||
queryCompanyPageUrl: ${OA_QUERY_COMPANY_PAGE_URL:/gwzh/OA/OA_GET_COMPANY_LIST}
|
||||
queryDepartmentPageUrl: ${OA_QUERY_DEPARTMENT_PAGE_URL:/gwzh/OA/OA_GET_DEPARTMENT_LIST}
|
||||
queryPersonPageUrl: ${OA_QUERY_PERSON_PAGE_URL:/gwzh/OA/OA_GET_USER_LIST}
|
||||
authorization: ${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-transport-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-lbs-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-user-api</artifactId>
|
||||
@@ -39,6 +43,10 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-system-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-process-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-bfi-api</artifactId>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user