diff --git a/blade-auth/pom.xml b/blade-auth/pom.xml index b0887270..8bfe4ea1 100644 --- a/blade-auth/pom.xml +++ b/blade-auth/pom.xml @@ -63,6 +63,10 @@ org.springblade blade-user-api + + org.springblade + blade-dict-api + org.springblade blade-system-api @@ -71,6 +75,10 @@ org.springblade blade-mk-api + + org.springblade + blade-wechat-api + org.springblade blade-resource-api diff --git a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java index f587e7ec..e75bdb50 100644 --- a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java +++ b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java @@ -25,6 +25,7 @@ */ package org.springblade.auth.config; +import org.springblade.auth.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(BladeLogHandler logHandler) { + FilterRegistrationBean 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); diff --git a/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java b/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java index b9c8420b..b8f8ed3a 100644 --- a/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java +++ b/blade-auth/src/main/java/org/springblade/auth/constant/BladeAuthConstant.java @@ -32,4 +32,10 @@ package org.springblade.auth.constant; */ public interface BladeAuthConstant { + /** + * 小程序/登录短信验证码资源编号(对应后台 /resource/sms 的 smsCode) + */ + String LOGIN_SMS_CODE = "ali_reg"; + } + diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java new file mode 100644 index 00000000..8e207ad1 --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoResponseAdvice.java @@ -0,0 +1,94 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.auth.endpoint; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.feign.ISysClient; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.Collections; +import java.util.List; + +/** + * 增强 /oauth/user-info 响应,补充 permission 权限标识字段 + * + * @author Chill + */ +@Slf4j +@RequiredArgsConstructor +@RestControllerAdvice(assignableTypes = OAuth2TokenEndPoint.class) +public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice { + + private final ISysClient sysClient; + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return returnType.getMethod() != null && "userInfo".equals(returnType.getMethod().getName()); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (!(body instanceof BladeUser bladeUser)) { + return body; + } + OAuth2UserInfoVO userInfo = BeanUtil.copyProperties(bladeUser, OAuth2UserInfoVO.class); + if (userInfo == null) { + return body; + } + userInfo.setPermission(loadPermission(bladeUser.getRoleId())); + return userInfo; + } + + private List loadPermission(String roleId) { + if (Func.isBlank(roleId)) { + return Collections.emptyList(); + } + try { + R> result = sysClient.getPermissions(roleId); + if (result != null && result.isSuccess() && result.getData() != null) { + return result.getData(); + } + } catch (Exception exception) { + log.warn("加载用户权限标识失败, roleId={}", roleId, exception); + } + return Collections.emptyList(); + } + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoVO.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoVO.java new file mode 100644 index 00000000..528a490e --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/OAuth2UserInfoVO.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.auth.endpoint; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.secure.BladeUser; + +import java.io.Serial; +import java.util.List; + +/** + * OAuth用户信息(含权限标识,供小程序等客户端使用) + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "OAuth用户信息") +public class OAuth2UserInfoVO extends BladeUser { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 权限标识集合(菜单按钮 code) + */ + @Schema(description = "权限标识集合") + private List permission; + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java index 878c5261..b48ac804 100644 --- a/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java @@ -28,13 +28,14 @@ package org.springblade.auth.endpoint; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import lombok.SneakyThrows; +import org.springblade.auth.constant.BladeAuthConstant; import org.springblade.core.oauth2.props.OAuth2Properties; import org.springblade.core.oauth2.provider.OAuth2Request; import org.springblade.core.oauth2.service.OAuth2User; import org.springblade.core.oauth2.service.OAuth2UserService; import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.SM2Util; -import org.springblade.core.tool.utils.StringPool; import org.springblade.core.tool.utils.StringUtil; import org.springblade.resource.feign.ISmsClient; import org.springblade.resource.utils.SmsUtil; @@ -73,11 +74,14 @@ public class Oauth2SmsEndpoint { * 短信验证码发送 * * @param tenantId 租户ID - * @param phone 手机号 + * @param phone 手机号(SM2 加密) + * @param code 短信资源编号,默认 ali_reg(后台 /resource/sms) */ @SneakyThrows @PostMapping("/oauth/sms/send-validate") - public R sendValidate(@RequestParam String tenantId, @RequestParam String phone) { + public R sendValidate(@RequestParam String tenantId, + @RequestParam String phone, + @RequestParam(required = false) String code) { // 校验手机加密认证,防止恶意发送验证码 String decryptedPhone = SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey()); if (StringUtil.isBlank(decryptedPhone)) { @@ -90,8 +94,9 @@ public class Oauth2SmsEndpoint { if (oAuth2User == null) { return R.fail(USER_PHONE_NOT_FOUND); } - // 用户存在则发送验证码 - R result = smsClient.sendValidate(tenantId, StringPool.EMPTY, decryptedPhone); + // 使用指定短信资源(默认 ali_reg) + String smsResourceCode = Func.toStr(code, BladeAuthConstant.LOGIN_SMS_CODE); + R result = smsClient.sendValidate(tenantId, smsResourceCode, decryptedPhone); return result.isSuccess() ? R.data(result.getData(), SmsUtil.SEND_SUCCESS) : R.fail(SmsUtil.SEND_FAIL); } diff --git a/blade-auth/src/main/java/org/springblade/auth/filter/LogoutLogFilter.java b/blade-auth/src/main/java/org/springblade/auth/filter/LogoutLogFilter.java new file mode 100644 index 00000000..2a625652 --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/filter/LogoutLogFilter.java @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.auth.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; + } + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index c1dfecfb..e7c6a272 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -45,6 +45,7 @@ import org.springblade.core.redis.cache.BladeRedis; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.StringPool; +import org.springblade.system.cache.DictCache; import org.springblade.system.feign.IUserClient; import org.springblade.system.pojo.entity.User; import org.springblade.system.pojo.entity.UserInfo; @@ -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 result = userClient.userInfo(tenantId, accountNo); - if (result.isSuccess() && hasUser(result.getData())) { + if (result.isSuccess() && hasLoginAccess(result.getData())) { return result.getData(); } - log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}", - tenantId, accountNo, result.isSuccess()); + if (result.isSuccess() && hasUser(result.getData())) { + log.info("IAM统一身份认证账号缺少有效角色或部门,开始补齐默认配置,tenantId={}, accountNo={}", tenantId, accountNo); + } else { + log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}", + tenantId, accountNo, result.isSuccess()); + } R saveResult = userClient.saveIamUser(buildIamUser(profileResponse, tenantId, accountNo)); if (!saveResult.isSuccess() || !Boolean.TRUE.equals(saveResult.getData())) { - log.warn("IAM统一身份认证自动创建用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, saveResult.getMsg()); - throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); + String failMsg = StringUtil.isNotBlank(saveResult.getMsg()) ? saveResult.getMsg() : OAuth2TokenConstant.USER_HAS_NO_ROLE; + log.warn("IAM统一身份认证自动创建或补齐用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, failMsg); + throw new UserInvalidException(failMsg); } R createdResult = userClient.userInfo(tenantId, accountNo); if (!createdResult.isSuccess() || !hasUser(createdResult.getData())) { log.warn("IAM统一身份认证自动创建用户后未查询到用户,tenantId={}, accountNo={}", tenantId, accountNo); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); } + if (!hasRoles(createdResult.getData())) { + log.warn("IAM统一身份认证自动创建用户后仍缺少可登录角色,tenantId={}, accountNo={}", tenantId, accountNo); + throw new UserInvalidException(OAuth2TokenConstant.USER_HAS_NO_ROLE); + } return createdResult.getData(); } @@ -191,6 +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 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) { diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java index 0c37eee5..540095c0 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java @@ -26,6 +26,7 @@ package org.springblade.auth.granter; import jakarta.servlet.http.HttpServletRequest; +import org.springblade.auth.constant.BladeAuthConstant; import org.springblade.core.oauth2.constant.OAuth2TokenConstant; import org.springblade.core.oauth2.exception.UserInvalidException; import org.springblade.core.oauth2.granter.AbstractTokenGranter; @@ -37,8 +38,8 @@ import org.springblade.core.oauth2.service.OAuth2User; import org.springblade.core.oauth2.service.OAuth2UserService; import org.springblade.core.sms.model.SmsCode; import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.SM2Util; -import org.springblade.core.tool.utils.StringPool; import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.WebUtil; import org.springblade.resource.feign.ISmsClient; @@ -78,8 +79,10 @@ public class SmsTokenGranter extends AbstractTokenGranter { if (StringUtil.isBlank(decryptedPhone)) { throw new UserInvalidException(OAuth2TokenConstant.USER_PHONE_NOT_FOUND); } - // 获取短信验证信息 - R result = smsClient.validateMessage(tenantId, StringPool.EMPTY, smsCode.getId(), smsCode.getValue(), decryptedPhone); + // 与发送时使用同一短信资源编号(默认 ali_reg) + HttpServletRequest httpRequest = WebUtil.getRequest(); + String smsResourceCode = Func.toStr(httpRequest.getParameter("code"), BladeAuthConstant.LOGIN_SMS_CODE); + R result = smsClient.validateMessage(tenantId, smsResourceCode, smsCode.getId(), smsCode.getValue(), decryptedPhone); if (!result.isSuccess()) { throw new UserInvalidException(OAuth2TokenConstant.CAPTCHA_NOT_CORRECT); } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java new file mode 100644 index 00000000..1bdd67bf --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java @@ -0,0 +1,102 @@ +package org.springblade.auth.granter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.oauth2.exception.UserInvalidException; +import org.springblade.core.oauth2.granter.AbstractTokenGranter; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springblade.system.feign.IUserClient; +import org.springblade.thirdparty.wechat.constant.WechatMiniConstant; +import org.springblade.thirdparty.wechat.exception.WechatMiniException; +import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO; +import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO; +import org.springblade.thirdparty.wechat.service.IWechatMiniService; +import org.springframework.stereotype.Component; + +/** + * 微信小程序手机号一键登录。 + *

+ * grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。 + * 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。 + */ +@Slf4j +@Component +public class WechatMiniTokenGranter extends AbstractTokenGranter { + + private final OAuth2UserService userService; + private final IWechatMiniService wechatMiniService; + private final IUserClient userClient; + + public WechatMiniTokenGranter(OAuth2ClientService clientService, + OAuth2UserService userService, + PasswordHandler passwordHandler, + IWechatMiniService wechatMiniService, + IUserClient userClient) { + super(clientService, userService, passwordHandler); + this.userService = userService; + this.wechatMiniService = wechatMiniService; + this.userClient = userClient; + } + + @Override + public String type() { + return WechatMiniConstant.GRANT_TYPE; + } + + @Override + public OAuth2User user(OAuth2Request request) { + // 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败 + var oauthClient = client(request); + + HttpServletRequest httpRequest = WebUtil.getRequest(); + String loginCode = httpRequest.getParameter("loginCode"); + String phoneCode = httpRequest.getParameter("phoneCode"); + if (StringUtil.isBlank(loginCode) || StringUtil.isBlank(phoneCode)) { + throw new UserInvalidException("微信登录参数不完整"); + } + + WechatSessionVO session; + WechatPhoneVO phoneInfo; + try { + session = wechatMiniService.code2Session(loginCode); + phoneInfo = wechatMiniService.getPhoneNumber(phoneCode); + } catch (WechatMiniException e) { + log.warn("微信小程序登录失败: {}", e.getMessage()); + throw new UserInvalidException(e.getMessage()); + } + + String phone = Func.toStr(phoneInfo.getPurePhoneNumber(), phoneInfo.getPhoneNumber()); + if (StringUtil.isBlank(phone)) { + throw new UserInvalidException("未获取到微信手机号"); + } + + OAuth2User user = userService.loadByPhone(phone, request); + if (!userService.validateUser(user)) { + throw new UserInvalidException("用户不存在,无法登录"); + } + + R bindResult = userClient.bindWxMiniOpenId( + request.getTenantId(), + Func.toLong(user.getUserId()), + session.getOpenid(), + phone + ); + if (bindResult == null || !bindResult.isSuccess()) { + String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败"; + log.warn("绑定微信 openid 失败 userId={} openid={} msg={}", user.getUserId(), session.getOpenid(), msg); + throw new UserInvalidException(StringUtil.isBlank(msg) ? "绑定 openid 失败" : msg); + } + + user.setClient(oauthClient); + return user; + } + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java index e6e793eb..2c12576a 100644 --- a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java +++ b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java @@ -154,7 +154,11 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler { */ @Override public void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation) { - // 自定义认证失败回调 + log.error("用户:{},认证失败,失败原因:{},grantType={},authorities={}", + user == null ? request.getUsername() : user.getAccount(), + validation.getMessage(), + request.getGrantType(), + user == null ? null : user.getAuthorities()); } /** diff --git a/blade-auth/src/main/java/org/springblade/auth/handler/BladeLogHandler.java b/blade-auth/src/main/java/org/springblade/auth/handler/BladeLogHandler.java index a57b4fcd..1d6bfbb9 100644 --- a/blade-auth/src/main/java/org/springblade/auth/handler/BladeLogHandler.java +++ b/blade-auth/src/main/java/org/springblade/auth/handler/BladeLogHandler.java @@ -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(); + } + } diff --git a/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java b/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java index ff74ff23..85158597 100644 --- a/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java +++ b/blade-auth/src/main/java/org/springblade/auth/props/IamSsoProperties.java @@ -78,7 +78,8 @@ public class IamSsoProperties { private String authorization; /** - * IAM用户信息认证头。为空时默认使用 Bearer accessToken + * IAM业务侧 Auth 请求头。为空时不传 Auth; + * Authorization 统一使用网关凭证 {@link #authorization}。 */ private String profileAuthorization; diff --git a/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java b/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java index ee43e92f..db727e84 100644 --- a/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java +++ b/blade-auth/src/main/java/org/springblade/auth/service/BladeClientDetailService.java @@ -25,11 +25,17 @@ */ package org.springblade.auth.service; +import org.springblade.core.oauth2.constant.OAuth2GranterConstant; import org.springblade.core.oauth2.provider.OAuth2Request; import org.springblade.core.oauth2.service.OAuth2Client; import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; import org.springframework.jdbc.core.JdbcTemplate; +import java.util.Arrays; +import java.util.Optional; + /** * BladeClientDetailService * @@ -57,6 +63,13 @@ public class BladeClientDetailService extends OAuth2ClientDetailService { @Override public boolean validateGranter(OAuth2Client client, String grantType) { - return super.validateGranter(client, grantType); + // 微信小程序一键登录:兼容库表未配置 wechat_applet 的存量客户端 + if (OAuth2GranterConstant.WECHAT_APPLET.equals(grantType) || "wechat_mini".equals(grantType)) { + return true; + } + return Optional.ofNullable(client) + .map(c -> Arrays.stream(Func.split(c.getAuthorizedGrantTypes(), StringPool.COMMA)) + .anyMatch(s -> s.trim().equals(grantType))) + .orElse(false); } } diff --git a/blade-auth/src/main/resources/application-dev.yml b/blade-auth/src/main/resources/application-dev.yml new file mode 100644 index 00000000..feb2ba40 --- /dev/null +++ b/blade-auth/src/main/resources/application-dev.yml @@ -0,0 +1,6 @@ +# 本机多网卡时 Nacos 易误选 169.254.*(链路本地),导致网关 Host is down +spring: + cloud: + nacos: + discovery: + ip: ${NACOS_DISCOVERY_IP:127.0.0.1} diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index 586debc0..6edfb410 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -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==} diff --git a/blade-common/pom.xml b/blade-common/pom.xml index 5fb0c220..a81cd931 100644 --- a/blade-common/pom.xml +++ b/blade-common/pom.xml @@ -22,6 +22,25 @@ org.springblade blade-starter-loadbalancer + + org.springblade + blade-starter-log + + provided + + + io.swagger.core.v3 + swagger-annotations + provided + + + org.springframework.boot + spring-boot-starter-aop + provided + org.springblade blade-core-auto diff --git a/blade-common/src/main/java/org/springblade/common/aspect/OperationApiLogAspect.java b/blade-common/src/main/java/org/springblade/common/aspect/OperationApiLogAspect.java new file mode 100644 index 00000000..62b7c76d --- /dev/null +++ b/blade-common/src/main/java/org/springblade/common/aspect/OperationApiLogAspect.java @@ -0,0 +1,145 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.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 event = new HashMap<>(16); + event.put(EventConstant.EVENT_LOG, logApi); + SpringUtil.publishEvent(new ApiLogEvent(event)); + } + +} diff --git a/blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java b/blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java index 7e8c2bf7..ac8f3c43 100644 --- a/blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java +++ b/blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java @@ -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") diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java index 70d7183d..6b0ad40d 100644 --- a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java @@ -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> 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)); } + /** + * 文本归一化:去掉空白、星号与中英文标点并统一小写 + *

+ * 目的是让「失败原因里写的列名」与「表头列名」能直接做包含匹配, + * 不受「*」「/」「:」等写法差异影响。 + */ + private static String normalize(String value) { + return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》//、-]", "").toLowerCase(); + } + + /** + * 取某一列用于定位的候选关键词 + *

+ * 包含表头列名、字段名,以及列名按分隔符拆出的片段 + * (如「车牌号/船号」拆出「车牌号」「船号」,失败原因只写其中一段时也能定位)。 + */ + private static List columnKeywords(Field field) { + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + String[] value = excelProperty.value(); + String columnName = value.length == 0 ? field.getName() : value[0]; + List keywords = new ArrayList<>(); + keywords.add(columnName); + keywords.add(field.getName()); + keywords.addAll(Arrays.asList(columnName.replace("*", "").split("[//、()()\\s]+"))); + return keywords.stream() + .map(ImportFailureExcelUtil::normalize) + .filter(keyword -> keyword.length() >= 2) + .distinct() + .toList(); + } + private static class ImportFailureCellStyleHandler implements CellWriteHandler { private final int failureReasonColumnIndex; + private final boolean markErrorColumns; + private final List> rows; + private final Map> redColumnsByRow = new HashMap<>(); private final Map redStyleCache = new HashMap<>(); private final Map noWrapStyleCache = new HashMap<>(); private final Map columnWidthCache = new HashMap<>(); - private ImportFailureCellStyleHandler(int failureReasonColumnIndex) { - this.failureReasonColumnIndex = failureReasonColumnIndex; + private ImportFailureCellStyleHandler(List excelFields, List> rows, boolean markErrorColumns) { + this.failureReasonColumnIndex = excelFields.size(); + this.markErrorColumns = markErrorColumns; + this.rows = rows; + if (markErrorColumns) { + List> 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); } } + /** + * 计算某一行需要标红的列 + *

+ * 失败原因里出现的列名即视为出错列;但当某个命中片段被另一列更长的命中 + * 片段完全覆盖时(如原因「车船类型不能为空」同时命中「车船类型」和它的 + * 子串「类型」),只保留更长的那一列,避免把无关列一起标红。 + */ + private Set resolveRedColumns(List row, List> columnKeywords) { + Set redColumns = new LinkedHashSet<>(); + redColumns.add(failureReasonColumnIndex); + String failureReason = normalize(String.valueOf(row.get(failureReasonColumnIndex))); + if (failureReason.isEmpty()) { + return redColumns; + } + List 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 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 redColumns = redColumnsByRow.get(rowIndex); + return redColumns != null && redColumns.contains(columnIndex); + } + + /** + * 标红单元格 + *

+ * 统一只改字体颜色,不加底纹;空单元格的红色字体在屏幕上不可见, + * 这类错误依靠失败原因列的文字定位。 + */ private void markRed(Cell cell) { CellStyle currentStyle = cell.getCellStyle(); CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { diff --git a/blade-common/src/main/java/org/springblade/common/excel/LenientDateParser.java b/blade-common/src/main/java/org/springblade/common/excel/LenientDateParser.java new file mode 100644 index 00000000..750c101a --- /dev/null +++ b/blade-common/src/main/java/org/springblade/common/excel/LenientDateParser.java @@ -0,0 +1,198 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.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)。 + *

+ * 解析规则: + *

    + *
  • 分隔符 {@code -}、{@code /}、{@code .} 均接受;补零与否均可(2026-08-02 ≡ 2026-8-2)
  • + *
  • 日期时间:日期部分同上,时间 HH:mm:ss,可省略秒或秒+分(2026-8-2 12:3 可解析)
  • + *
  • 拒绝:两位年份(26-8-2)、日在前(2/8/2026)、无分隔符(20260802)
  • + *
+ * + * @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; + } + +} diff --git a/blade-common/src/main/java/org/springblade/common/excel/LenientDateStringConverter.java b/blade-common/src/main/java/org/springblade/common/excel/LenientDateStringConverter.java new file mode 100644 index 00000000..970dde72 --- /dev/null +++ b/blade-common/src/main/java/org/springblade/common/excel/LenientDateStringConverter.java @@ -0,0 +1,75 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.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 文本,文本原样透传。 + *

+ * 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错—— + * 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取, + * 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。 + * + * @author Chill + */ +public class LenientDateStringConverter implements Converter { + + @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); + } + +} diff --git a/blade-common/src/main/java/org/springblade/common/excel/LenientDateTimeStringConverter.java b/blade-common/src/main/java/org/springblade/common/excel/LenientDateTimeStringConverter.java new file mode 100644 index 00000000..bcb070a2 --- /dev/null +++ b/blade-common/src/main/java/org/springblade/common/excel/LenientDateTimeStringConverter.java @@ -0,0 +1,82 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.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} 文本,文本原样透传。 + *

+ * 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错—— + * 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取, + * 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。 + * + * @author Chill + */ +public class LenientDateTimeStringConverter implements Converter { + + /** + * 数值日期序列号转文本的输出格式,与导入模板展示格式保持一致。 + */ + 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); + } + +} diff --git a/blade-common/src/main/java/org/springblade/common/utils/PasswordRuleUtil.java b/blade-common/src/main/java/org/springblade/common/utils/PasswordRuleUtil.java new file mode 100644 index 00000000..b314e331 --- /dev/null +++ b/blade-common/src/main/java/org/springblade/common/utils/PasswordRuleUtil.java @@ -0,0 +1,106 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.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 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(); + } + +} diff --git a/blade-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/blade-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..33963739 --- /dev/null +++ b/blade-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springblade.common.config.BladeCommonConfiguration diff --git a/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java b/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java index 86787cf5..c324c64d 100644 --- a/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java +++ b/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java @@ -48,6 +48,7 @@ public class AuthProvider { DEFAULT_SKIP_URL.add("/oauth/sms/**"); DEFAULT_SKIP_URL.add("/oauth/clear-cache/**"); DEFAULT_SKIP_URL.add("/oauth/user-info"); + DEFAULT_SKIP_URL.add("/oauth/logout/**"); DEFAULT_SKIP_URL.add("/oauth/render/**"); DEFAULT_SKIP_URL.add("/oauth/callback/**"); DEFAULT_SKIP_URL.add("/oauth/revoke/**"); @@ -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"); } /** diff --git a/blade-gateway/src/main/resources/application-dev.yml b/blade-gateway/src/main/resources/application-dev.yml index b8eaf546..3d12a7a4 100644 --- a/blade-gateway/src/main/resources/application-dev.yml +++ b/blade-gateway/src/main/resources/application-dev.yml @@ -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 diff --git a/blade-ops/blade-resource/src/main/resources/application-dev.yml b/blade-ops/blade-resource/src/main/resources/application-dev.yml index 2ef60321..acf0e796 100644 --- a/blade-ops/blade-resource/src/main/resources/application-dev.yml +++ b/blade-ops/blade-resource/src/main/resources/application-dev.yml @@ -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} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java index 16cc18ea..ecbdf0c6 100644 --- a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java @@ -38,6 +38,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> queryApprovedRecordsNoAttachments(@RequestParam(name = "bizId", required = false) String bizId, @RequestParam(name = "processInstanceId", required = false) String processInstanceId); + + /** + * 获取流程当前节点详情 + * + * @param processInstanceId 流程实例id + * @param loginName MK登录名(手机号) + * @return 当前节点详情 + */ + @GetMapping(GET_CURRENT_NODES) + FR getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam(value = "loginName", required = false) String loginName); + + /** + * 获取流程实例详情 + * + * @param processInstanceId 流程实例id + * @param loginName MK登录名(手机号) + * @return 流程实例详情 + */ + @GetMapping(GET_PROCESS_INFO) + FR getProcessInfo(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam(value = "loginName", required = false) String loginName); } diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java index 4c1b36bb..93510a64 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java @@ -73,6 +73,7 @@ public interface ISysClient { String REGION = API_PREFIX + "/region"; String FEE_ITEMS = API_PREFIX + "/fee-items"; String CARGO_TYPES = API_PREFIX + "/cargo-types"; + String PERMISSIONS = API_PREFIX + "/permissions"; String INVOICE_ITEMS = API_PREFIX + "/invoice-items"; /** @@ -317,6 +318,15 @@ public interface ISysClient { @GetMapping(CARGO_TYPES) R> getCargoTypes(); + /** + * 获取角色权限标识集合(按钮编号) + * + * @param roleId 角色id + * @return 权限标识 + */ + @GetMapping(PERMISSIONS) + R> getPermissions(@RequestParam("roleId") String roleId); + /** * 获取启用的开票项目(含税收分类编码,开票申请推送金蝶按分类+简称匹配取数) * diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java index 0bfe39a1..a750235c 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java @@ -174,6 +174,11 @@ public class ISysClientFallback implements ISysClient { return R.fail("获取数据失败"); } + @Override + public R> getPermissions(String roleId) { + return R.fail("获取数据失败"); + } + @Override public R> getInvoiceItems() { return R.fail("获取数据失败"); diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java index 2e075ecf..932e41a7 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java @@ -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; + } diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java index ad80a1e9..7cdde906 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/MeasurementUnit.java @@ -43,6 +43,12 @@ public class MeasurementUnit extends BaseEntity { @Serial private static final long serialVersionUID = 1L; + /** + * 计量单位编码 + */ + @Schema(description = "计量单位编码") + private String unitCode; + /** * 计量单位 */ diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java new file mode 100644 index 00000000..590a8baf --- /dev/null +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * OA组织(公司/部门)分页同步结果 + * + * @author Chill + */ +@Data +@Schema(description = "OA组织分页同步结果") +public class OaOrgSyncPageVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "同步阶段:company / department") + private String stage; + + @Schema(description = "当前页") + private Integer current; + + @Schema(description = "每页条数") + private Integer size; + + @Schema(description = "OA总条数") + private Long total; + + @Schema(description = "本页从OA拉取的条数") + private Integer fetchedCount; + + @Schema(description = "本页同步成功条数") + private Integer syncedCount; + + @Schema(description = "本页跳过条数") + private Integer skippedCount; + + @Schema(description = "是否已到最后一页") + private Boolean finished; +} diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java new file mode 100644 index 00000000..b493bdf3 --- /dev/null +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaPersonSyncPageVO.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * OA人员分页同步结果 + * + * @author Chill + */ +@Data +@Schema(description = "OA人员分页同步结果") +public class OaPersonSyncPageVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "当前页") + private Integer current; + + @Schema(description = "每页条数") + private Integer size; + + @Schema(description = "OA人员总条数") + private Long total; + + @Schema(description = "本页从OA拉取的条数") + private Integer fetchedCount; + + @Schema(description = "本页同步成功条数") + private Integer syncedCount; + + @Schema(description = "本页跳过条数") + private Integer skippedCount; + + @Schema(description = "是否已到最后一页") + private Boolean finished; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/ICustomerArchiveClient.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/ICustomerArchiveClient.java new file mode 100644 index 00000000..53c7aa25 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/ICustomerArchiveClient.java @@ -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 syncProcessNode(@RequestBody CustomerProcessNodeSyncDTO param); +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/IMkProcessClient.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/IMkProcessClient.java new file mode 100644 index 00000000..e19dbb81 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/feign/IMkProcessClient.java @@ -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 apply(@RequestBody MkProcessSyncDTO param); +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CustomerProcessNodeSyncDTO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CustomerProcessNodeSyncDTO.java new file mode 100644 index 00000000..f30493ae --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/CustomerProcessNodeSyncDTO.java @@ -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; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/MkProcessSyncDTO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/MkProcessSyncDTO.java new file mode 100644 index 00000000..919b1923 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/MkProcessSyncDTO.java @@ -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; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java index 6890fa1b..5ea8bec1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java @@ -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; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerReceiptAccount.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerReceiptAccount.java index e491a213..c0c67d5a 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerReceiptAccount.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerReceiptAccount.java @@ -59,6 +59,9 @@ public class CustomerReceiptAccount extends TenantEntity { @Schema(description = "开户行名称") private String bankName; + @Schema(description = "联行号") + private String cnapsCode; + @Schema(description = "银行账号") private String bankAccount; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminDriverOptionVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminDriverOptionVO.java new file mode 100644 index 00000000..21839e5e --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminDriverOptionVO.java @@ -0,0 +1,30 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Schema(description = "调度端司机搜索项") +public class AdminDriverOptionVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "司机ID") + private Long id; + + @Schema(description = "姓名") + private String name; + + @Schema(description = "手机号") + private String phone; + + @Schema(description = "绑定车牌") + private String vehicleNo; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeBadgesVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeBadgesVO.java new file mode 100644 index 00000000..d37bd5bd --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeBadgesVO.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 调度端首页快捷入口角标 + */ +@Data +@Schema(description = "调度端首页角标") +public class AdminHomeBadgesVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "异常处置待办数(disposalStatus≠completed)") + private long exception; + + @Schema(description = "风险记录待办数(disposalStatus=pending)") + private long risk; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeStatsVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeStatsVO.java new file mode 100644 index 00000000..b3b97705 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminHomeStatsVO.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 调度端运单详情(对齐小程序 pages/waybill/detail) + */ +@Data +@Schema(description = "调度端运单详情") +public class AdminWaybillDetailVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long id; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "状态:0待接单/1运输中/2已完成/3已取消") + private Integer status; + + @Schema(description = "起点名称") + private String fromName; + + @Schema(description = "终点名称") + private String toName; + + @Schema(description = "起点地址") + private String fromAddress; + + @Schema(description = "终点地址") + private String toAddress; + + @Schema(description = "装货地址") + private String pickupAddress; + + @Schema(description = "卸货地址") + private String unloadAddress; + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "货物数量(带单位)") + private String cargoQuantity; + + @Schema(description = "重量(带单位)") + private String weight; + + @Schema(description = "合计货重") + private String totalWeight; + + @Schema(description = "运输方式文案:公路运输 / 铁路运输 等") + private String transportType; + + @Schema(description = "运输方式字典值:road 等") + private String transportMode; + + @Schema(description = "运输组织:common普通 / load配载") + private String transportOrgType; + + @Schema(description = "计划发货时间") + private String planShipTime; + + @Schema(description = "计划完成时间") + private String planFinishTime; + + @Schema(description = "承运方") + private String carrierName; + + @Schema(description = "司机姓名") + private String driverName; + + @Schema(description = "司机联系方式") + private String driverPhone; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否有未完成异常") + private Boolean hasException; + + @Schema(description = "最近一条未完成异常处置ID(有异常时返回)") + private Long exceptionId; + + @Schema(description = "需重新派单") + private Boolean needReassign; + + @Schema(description = "司机接单状态:pending/accepted/rejected") + private String acceptStatus; + + @Schema(description = "司机拒绝接单原因") + private String rejectReason; + + @Schema(description = "原指派司机ID") + private Long driverId; + + @Schema(description = "装卸点列表") + private List routePoints; + + @Schema(description = "过程打卡节点(与司机端 punchNodes 同结构)") + private List punchNodes; + + @Schema(description = "途打卡记录") + private List enrouteRecords; + + @Data + @Schema(description = "装卸点") + public static class AdminRoutePointVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "点位名称") + private String name; + + @Schema(description = "详细地址") + private String address; + + @Schema(description = "状态:pending/done/active") + private String status; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java index bd1befa2..802ed9ed 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java @@ -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; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java index 162b3860..57e394d1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java @@ -133,4 +133,55 @@ public class DriverWaybillCardVO implements Serializable { @Schema(description = "过程配置中 punch=是 的打卡节点列表(详情返回)") private List punchNodes; + /* ===== pages/waybill/detail 接单查看字段 ===== */ + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "装货地址") + private String pickupAddress; + + @Schema(description = "卸货地址") + private String unloadAddress; + + @Schema(description = "货物数量(带单位)") + private String cargoQuantity; + + @Schema(description = "运输方式文案:公路运输等") + private String transportType; + + @Schema(description = "计划发货时间") + private String planShipTime; + + @Schema(description = "计划完成时间") + private String planFinishTime; + + @Schema(description = "合计货重") + private String totalWeight; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "装卸点(详情返回)") + private List routePoints; + + @Schema(description = "过程配置 JSON(详情返回,供前端兜底推导打卡节点)") + private String processJson; + + @Data + @Schema(description = "司机端装卸点") + public static class DriverRoutePointVO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "点位名称") + private String name; + + @Schema(description = "详细地址") + private String address; + + @Schema(description = "状态:pending/done/active") + private String status; + } + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java index c2ce78f0..f576efd2 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingManageVO.java @@ -46,6 +46,10 @@ public class LoadingManageVO extends LoadingManage { @Schema(description = "业务状态名称") private String businessStatusName; + @TableField(exist = false) + @Schema(description = "是否存在司机拒绝接单(可重新派单)") + private Boolean driverRejected; + @TableField(exist = false) @Schema(description = "运单号") private String waybillNo; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java index 0b6fa770..aed01d69 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java @@ -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> cargoTypeOptions; + + @TableField(exist = false) + @Schema(description = "运输类型字典,公开页回显使用") + private List> transportTypeOptions; + + @TableField(exist = false) + @Schema(description = "结算方式字典,公开页回显使用") + private List> settlementModeOptions; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java index a8d394db..49e6e89d 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportPlanVO.java @@ -27,8 +27,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.TransportPlan; +import org.springframework.format.annotation.DateTimeFormat; import java.io.Serial; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; /** @@ -72,5 +75,24 @@ public class TransportPlanVO extends TransportPlan { @Schema(description = "计划调度生成的运单") private List dispatchRows; + @TableField(exist = false) + @Schema(description = "计划开始日期起") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate planStartDateStart; + + @TableField(exist = false) + @Schema(description = "计划开始日期止") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate planStartDateEnd; + + @TableField(exist = false) + @Schema(description = "创建开始时间") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTimeStart; + + @TableField(exist = false) + @Schema(description = "创建结束时间") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTimeEnd; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillLocateVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillLocateVO.java new file mode 100644 index 00000000..c3ce7967 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillLocateVO.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Map; + +/** + * 运单车辆实时定位结果 + * + * @author Chill + */ +@Data +@Schema(description = "运单车辆实时定位结果") +public class WaybillLocateVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "地址") + private String address; + + @Schema(description = "定位时间") + private String locateTime; + + @Schema(description = "速度") + private String speed; + + @Schema(description = "方向") + private String direction; + + @Schema(description = "LBS原始数据") + private Map rawData; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillTrackVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillTrackVO.java new file mode 100644 index 00000000..44a9e1b7 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillTrackVO.java @@ -0,0 +1,95 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 运单历史轨迹结果 + * + * @author Chill + */ +@Data +@Schema(description = "运单历史轨迹结果") +public class WaybillTrackVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "开始日期") + private String startDate; + + @Schema(description = "结束日期") + private String endDate; + + @Schema(description = "轨迹点数量") + private Integer total; + + @Schema(description = "轨迹点列表") + private List points = new ArrayList<>(); + + @Data + @Schema(description = "轨迹点") + public static class WaybillTrackPointVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "车牌号") + private String vehicleNo; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "定位时间") + private String locateTime; + + @Schema(description = "速度") + private String speed; + + @Schema(description = "方向") + private String direction; + + @Schema(description = "地址") + private String address; + } +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java index cc78a955..50be69e0 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java @@ -61,6 +61,7 @@ public interface IUserClient { String SAVE_IAM_USER = API_PREFIX + "/save-iam-user"; String REGISTER_USER = API_PREFIX + "/register-user"; String REMOVE_USER = API_PREFIX + "/remove-user"; + String BIND_WX_MINI_OPENID = API_PREFIX + "/bind-wx-mini-openid"; /** * 获取用户信息 @@ -187,4 +188,18 @@ public interface IUserClient { @PostMapping(REMOVE_USER) R removeUser(@RequestParam("tenantIds") String tenantIds); + /** + * 绑定微信小程序 openid(写入 blade_user_oauth,source=WECHAT_MINI) + * + * @param tenantId 租户ID + * @param userId 用户ID + * @param openid 微信 openid + * @param phone 手机号(可选,写入 username) + */ + @PostMapping(BIND_WX_MINI_OPENID) + R bindWxMiniOpenId(@RequestParam("tenantId") String tenantId, + @RequestParam("userId") Long userId, + @RequestParam("openid") String openid, + @RequestParam(value = "phone", required = false) String phone); + } diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneChangeDTO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneChangeDTO.java new file mode 100644 index 00000000..17433768 --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneChangeDTO.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 更换手机号参数 + * + * @author Chill + */ +@Data +@Schema(description = "更换手机号参数") +public class PhoneChangeDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "新手机号", requiredMode = Schema.RequiredMode.REQUIRED) + private String newPhone; + + @Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "新手机号短信验证码", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneVerifyDTO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneVerifyDTO.java new file mode 100644 index 00000000..79520b8f --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/dto/PhoneVerifyDTO.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 原手机号短信校验参数 + * + * @author Chill + */ +@Data +@Schema(description = "原手机号短信校验参数") +public class PhoneVerifyDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "短信校验 ID(发送验证码接口返回)", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; +} diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java index e76743f7..9be79473 100644 --- a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/OpenApiApplication.java @@ -44,6 +44,10 @@ public class OpenApiApplication { public static void main(String[] args) { BladeApplication.disableNacosLaunchConfig(); + // 当前处理人刷新依赖 RedisLockClient;Nacos 全局 blade.lock.enabled=false 时仍需为本服务开启 + if (System.getProperty("blade.lock.enabled") == null) { + System.setProperty("blade.lock.enabled", "true"); + } BladeApplication.run(AppConstant.APPLICATION_OPENAPI_NAME, OpenApiApplication.class, args); } diff --git a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java index 0f43c46d..727fce2e 100644 --- a/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java +++ b/blade-service/blade-openapi/src/main/java/org/springblade/openapi/mk/Api4MK.java @@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.Hidden; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.secure.constant.AuthConstant; import org.springblade.core.tool.api.FR; import org.springblade.openapi.mk.api.IApi4MK; import org.springblade.openapi.mk.pojo.dto.Api4MKProcessApprovalDTO; @@ -49,6 +51,7 @@ public class Api4MK implements IApi4MK { } @Override + @PreAuth(AuthConstant.PERMIT_ALL) public FR processCommonCallback(Api4MKProcessApprovalDTO param) { log.info("mk流程通用回调 操作名称:{} 参数:{}", ProcessOperationType.getOperationName(param.getOperation()), JSON.toJSONString(param)); callback(param, ProcessHandler::approve); diff --git a/blade-service/blade-openapi/src/main/resources/application.yml b/blade-service/blade-openapi/src/main/resources/application.yml index be5d93fe..28686324 100644 --- a/blade-service/blade-openapi/src/main/resources/application.yml +++ b/blade-service/blade-openapi/src/main/resources/application.yml @@ -10,6 +10,7 @@ spring: - nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true - nacos:blade-openapi-dynamictp.yaml?group=DEFAULT_GROUP&refreshEnabled=true - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true + - optional:classpath:openapi-lock.yaml cloud: nacos: username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}} diff --git a/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml b/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml new file mode 100644 index 00000000..e388d4f8 --- /dev/null +++ b/blade-service/blade-openapi/src/main/resources/openapi-lock.yaml @@ -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} diff --git a/blade-service/blade-system/pom.xml b/blade-service/blade-system/pom.xml index 08aea8b1..81ac5a93 100644 --- a/blade-service/blade-system/pom.xml +++ b/blade-service/blade-system/pom.xml @@ -45,6 +45,10 @@ org.springblade blade-user-api + + org.springblade + blade-resource-api + org.springblade blade-process-api @@ -58,6 +62,10 @@ org.springblade blade-mk-api + + org.springblade + blade-transport-api + diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java b/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java index c9765695..6f83dd9f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java @@ -17,6 +17,7 @@ import org.springblade.process.pojo.dto.ApprovalDTO; import org.springblade.process.pojo.vo.ApprovalVO; import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; import org.springblade.process.service.IBusinessProcessService; +import org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -52,29 +53,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 processSubmit(@Validated @RequestBody MKProcessCreateDTO param) { + return R.data(businessProcessService.processSubmit(param)); + } + + @PostMapping("/processDelete") + @ApiOperationSupport(order = 7) + public R 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 isEditView(@Valid @NotBlank(message = "业务id不能为空") String bizId) { return R.data(businessProcessService.isEditView(bizId)); } @GetMapping("/getMKApprovalUrl") - @ApiOperationSupport(order = 3) + @ApiOperationSupport(order = 4) @Operation(summary = "获取mk审批页链接", description = "传入业务id或流程实例id") public R getMKApprovalUrl(String bizId, String processInstanceId) { return R.data(businessProcessService.getMKApprovalUrl(bizId, processInstanceId)); } @GetMapping("/getApprovedRecords") - @ApiOperationSupport(order = 4) + @ApiOperationSupport(order = 5) @Operation(summary = "查询审批记录", description = "传入业务id或流程实例id") public R> getApprovedRecords(String bizId, String processInstanceId) { return R.data(businessProcessService.queryApprovedRecords(bizId, processInstanceId)); } @GetMapping("/downloadFile") - @ApiOperationSupport(order = 5) + @ApiOperationSupport(order = 6) @Operation(summary = "下载附件", description = "传入附件id") public void downloadFile(HttpServletResponse response, @Valid @NotBlank(message = "附件id不能为空") String fileId) { businessProcessService.downloadFile(response, fileId); diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java index eb789f44..fa87f975 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java @@ -3,6 +3,8 @@ package org.springblade.process.feign; import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.Valid; import lombok.AllArgsConstructor; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.secure.constant.AuthConstant; import org.springblade.core.tool.api.FR; import org.springblade.process.pojo.dto.BusinessProcessCurrentHandlerRefreshDTO; import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO; @@ -13,8 +15,10 @@ import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; import org.springblade.process.pojo.vo.ProcessTodoVO; import org.springblade.process.service.IBusinessProcessService; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -74,4 +78,20 @@ public class BusinessProcessClient implements IBusinessProcessClient { public FR> queryApprovedRecordsNoAttachments(String bizId, String processInstanceId) { return FR.data(businessProcessService.queryApprovedRecordsNoAttachments(bizId, processInstanceId)); } + + @PreAuth(AuthConstant.PERMIT_ALL) + @GetMapping(GET_CURRENT_NODES) + @Override + public FR getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam(value = "loginName", required = false) String loginName) { + return FR.data(businessProcessService.getCurrentNodes(processInstanceId, loginName)); + } + + @PreAuth(AuthConstant.PERMIT_ALL) + @GetMapping(GET_PROCESS_INFO) + @Override + public FR getProcessInfo(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam(value = "loginName", required = false) String loginName) { + return FR.data(businessProcessService.getProcessInfo(processInstanceId, loginName)); + } } diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java index 5e3e2ff8..e3b7834a 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.springblade.process.pojo.dto.*; import org.springblade.process.pojo.entity.BusinessProcess; import org.springblade.process.pojo.vo.*; +import org.springblade.thirdparty.mk.pojo.dto.MKProcessCreateDTO; import java.util.List; @@ -25,6 +26,40 @@ public interface IBusinessProcessService extends IService { */ 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 diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java index 50984d30..088c83d5 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java @@ -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 ServiceImpllambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + ); + if (businessProcess == null) { + businessProcess = new BusinessProcess(); + businessProcess.setBizId(bizId); + businessProcess.setProcessType(param.getTemplateCode()); + businessProcess.setSubject(param.getSubject()); + businessProcess.setPromoterId(AuthUtil.getUserId()); + businessProcess.setPromoterName(AuthUtil.getNickName()); + businessProcess.setPromoterLoginName(loginName); + businessProcess.setSubmitTime(new Date()); + } else { + businessProcess.setProcessType(param.getTemplateCode()); + if (StringUtil.isNotBlank(param.getSubject())) { + businessProcess.setSubject(param.getSubject()); + } + businessProcess.setPromoterLoginName(loginName); + if (businessProcess.getSubmitTime() == null) { + businessProcess.setSubmitTime(new Date()); + } + } + businessProcess.setProcessInstanceId(processInstanceId); + businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue()); + this.saveOrUpdate(businessProcess); + 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.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 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 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 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 readMkHandlerNames(Object node) { + List 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) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java index bbad903b..b55719ff 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java @@ -28,13 +28,16 @@ package org.springblade.system; import org.springblade.core.cloud.client.BladeCloudApplication; import org.springblade.core.launch.BladeApplication; import org.springblade.core.launch.constant.AppConstant; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.ComponentScan; +import org.springblade.system.props.IamSyncProperties; /** * 系统模块启动器 * @author Chill */ @ComponentScan(basePackages = {"org.springblade.system", "org.springblade.process"}) +@EnableConfigurationProperties(IamSyncProperties.class) @BladeCloudApplication public class SystemApplication { @@ -44,4 +47,3 @@ public class SystemApplication { } } - diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 11c4081a..180dc550 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -50,8 +50,10 @@ import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.User; import org.springblade.system.pojo.enums.DictEnum; import org.springblade.system.pojo.vo.DeptVO; +import org.springblade.system.pojo.vo.OaOrgSyncPageVO; import org.springblade.system.pojo.vo.UserVO; import org.springblade.system.service.IDeptService; +import org.springblade.system.service.IOASyncService; import org.springblade.system.wrapper.DeptWrapper; import org.springframework.web.bind.annotation.*; @@ -73,6 +75,7 @@ import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE; public class DeptController extends BladeController { private final IDeptService deptService; + private final IOASyncService oaSyncService; /** * 详情 @@ -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 syncIamOrganizations() { + return R.data(deptService.syncIamOrganizations()); + } + + /** + * 清除非顶级组织,供自动同步前确认后调用 + */ + @IsAdmin + @PostMapping("/clear-non-top") + @ApiOperationSupport(order = 8) + @Operation(summary = "清除非顶级组织") + public R 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 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 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> 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> 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> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) { List list = deptService.deptLeaderInfo(deptId); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java index 3979b3ca..0639b52d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java @@ -1,263 +1,276 @@ -/** - * BladeX Commercial License Agreement - * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. - *

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

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

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

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

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

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

- * Author: Chill Zhuang (bladejava@qq.com) - */ -package org.springblade.system.controller; - -import com.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 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> list(PortTerminalVO portTerminal, Query query) { - IPage 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> portSelect() { - List 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 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 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 queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class); - applyDataSourceCondition(queryWrapper, dataSource); - if (Func.isNotEmpty(ids)) { - queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString())); - } - List 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 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 params) { - Object regionCode = params.remove("regionCode"); - if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) { - params.put("districtCode", regionCode); - } - } - - private void applyDataSourceCondition(QueryWrapper 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. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.controller; + +import com.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 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> list(PortTerminalVO portTerminal, Query query) { + IPage 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> portSelect() { + List 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("操作成功"); + } + + /** + * 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。 + *

+ * 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。 + */ + 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 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 queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class); + applyDataSourceCondition(queryWrapper, dataSource); + if (Func.isNotEmpty(ids)) { + queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString())); + } + List 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 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 params) { + Object regionCode = params.remove("regionCode"); + if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) { + params.put("districtCode", regionCode); + } + } + + private void applyDataSourceCondition(QueryWrapper 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; + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java index 328fd2af..ab5e7820 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java @@ -54,7 +54,9 @@ import org.springblade.core.tool.utils.StringPool; import org.springblade.system.excel.UserExcel; import org.springblade.system.excel.UserImporter; import org.springblade.system.pojo.entity.User; +import org.springblade.system.pojo.vo.OaPersonSyncPageVO; import org.springblade.system.pojo.vo.UserVO; +import org.springblade.system.service.IOASyncService; import org.springblade.system.service.IUserService; import org.springblade.system.wrapper.UserWrapper; import org.springframework.web.bind.annotation.*; @@ -77,6 +79,7 @@ import java.util.Map; public class UserController { private final IUserService userService; + private final IOASyncService oaSyncService; /** * 查询单条 @@ -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 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); } + /** + * 当前用户设置/重置登录密码(小程序首次设密、短信验证后改密) + *

+ * 对外路径:/blade-system/user/password ;网关别名 /blade-user/password 亦可到达。 + */ + @PostMapping("/password") + @ApiOperationSupport(order = 10) + @Operation(summary = "设置登录密码", description = "当前登录用户设置密码,无需原密码") + public R password(BladeUser user, + @Parameter(description = "新密码", required = true) @RequestParam String password, + @Parameter(description = "确认密码", required = true) @RequestParam String password2) { + return R.status(userService.setPassword(user.getUserId(), password, password2)); + } + /** * 管理员修改密码 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java new file mode 100644 index 00000000..ecf7b715 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserPhoneController.java @@ -0,0 +1,93 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.controller; + +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springblade.core.tenant.annotation.NonDS; +import org.springblade.core.tool.api.R; +import org.springblade.system.pojo.dto.PhoneChangeDTO; +import org.springblade.system.pojo.dto.PhoneVerifyDTO; +import org.springblade.system.service.IUserPhoneService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户手机号变更(小程序「修改手机号」) + *

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

+ * 当前手机号、未占用的新手机号均可发送;新号若已被其他账号占用则拒绝。 + */ + @PostMapping("/send-code") + @ApiOperationSupport(order = 1) + @Operation(summary = "发送手机号变更验证码", description = "传入明文手机号,返回短信校验 id") + public R sendCode(@Parameter(description = "手机号", required = true) @RequestParam String phone) { + return userPhoneService.sendCode(phone); + } + + /** + * 校验原手机号验证码(修改手机号第 1 步) + */ + @PostMapping("/verify-old") + @ApiOperationSupport(order = 2) + @Operation(summary = "校验原手机号验证码", description = "传入发送验证码返回的 id 与验证码") + public R verifyOld(@Valid @RequestBody PhoneVerifyDTO phoneVerify) { + return R.status(userPhoneService.verifyOldPhone(phoneVerify)); + } + + /** + * 绑定新手机号(修改手机号第 3 步,需先完成 verify-old) + */ + @PostMapping("/change") + @ApiOperationSupport(order = 3) + @Operation(summary = "更换手机号", description = "传入新手机号及短信校验 id、验证码") + public R change(@Valid @RequestBody PhoneChangeDTO phoneChange) { + return R.status(userPhoneService.changePhone(phoneChange)); + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java index 3199b7ef..d46b3c82 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java @@ -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(); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java index 924daee0..28f7004b 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java @@ -64,11 +64,11 @@ public interface UserConvert { @Mapping(target = "password", ignore = true) @Mapping(target = "birthday", ignore = true) @Mapping(target = "sex", ignore = true) + @Mapping(target = "account", ignore = true) @Mapping(source = "workcode", target = "code") @Mapping(source = "lastname", target = "name") @Mapping(source = "lastname", target = "realName") @Mapping(source = "mobile", target = "phone") - @Mapping(source = "mobile", target = "account") @Mapping(source = "email", target = "email") User baseConvert(OAPersonResponse person); @@ -86,7 +86,7 @@ public interface UserConvert { */ default User person2user(OAPersonResponse person, String defaultPassword) { User user = baseConvert(person); - + user.setAccount(resolveAccount(person)); // 密码 user.setPassword(defaultPassword); // 性别 @@ -102,6 +102,28 @@ public interface UserConvert { return user; } + /** + * 解析本系统登录账号:优先 OA loginid,其次工号,最后手机号 + * + * @param person OA人员 + * @return 账号,无法识别时返回 null + */ + default String resolveAccount(OAPersonResponse person) { + if (person == null) { + return null; + } + if (StringUtils.isNotBlank(person.getLoginid())) { + return person.getLoginid().trim(); + } + if (StringUtils.isNotBlank(person.getWorkcode())) { + return person.getWorkcode().trim(); + } + if (StringUtils.isNotBlank(person.getMobile())) { + return person.getMobile().trim(); + } + return null; + } + /** * oa人员转本系统用户部门 * @param person @@ -120,7 +142,7 @@ public interface UserConvert { // 排序 userDept.setSort(OAUtils.parseInt(person.getDsporder())); // 用户id - userDept.setUserId(userMap.get(person.getMobile())); + userDept.setUserId(userMap.get(resolveAccount(person))); userDept.setSyncTime(new Date()); return userDept; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java index 941b7eb3..b59573d5 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java @@ -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; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java new file mode 100644 index 00000000..0277ec01 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.excel; + +import org.springblade.core.log.exception.ServiceException; + +import java.io.Serial; +import java.util.List; + +/** + * 导入失败异常,携带失败明细用于导出原表并标注错误。 + *

+ * 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚, + * 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。 + * + * @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; + } + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java index 1dd71acc..306e144f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java @@ -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; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java index b227c4a8..07e04660 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java @@ -232,6 +232,12 @@ public class SysClient implements ISysClient { .orderByAsc(CargoType::getCargoCode))); } + @Override + @GetMapping(PERMISSIONS) + public R> getPermissions(String roleId) { + return R.data(menuService.permissionCodes(roleId)); + } + @Override @GetMapping(INVOICE_ITEMS) public R> getInvoiceItems() { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java index 12b97fd0..f25da255 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java @@ -133,4 +133,10 @@ public class UserClient implements IUserClient { return R.data(service.remove(Wrappers.query().lambda().in(User::getTenantId, Func.toStrList(tenantIds)))); } + @Override + @PostMapping(BIND_WX_MINI_OPENID) + public R bindWxMiniOpenId(String tenantId, Long userId, String openid, String phone) { + return R.data(service.bindWxMiniOpenId(tenantId, userId, openid, phone)); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml index 387f1a27..10fbb903 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml @@ -13,6 +13,7 @@ + @@ -30,6 +31,7 @@ mmu.update_time, mmu.status, mmu.is_deleted, + mmu.unit_code, mmu.unit_name, mmu.dimension, mmu.remark @@ -39,6 +41,10 @@ LEFT JOIN blade_user uu ON uu.id = mmu.update_user WHERE mmu.is_deleted = 0 + + + AND mmu.unit_code LIKE #{unitCodeLike} + AND mmu.unit_name LIKE #{unitNameLike} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml index 6a4dd476..70dd2e9a 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml @@ -95,7 +95,7 @@