merge 合并5.x分支代码

This commit is contained in:
gssong
2023-07-29 13:41:38 +08:00
370 changed files with 9390 additions and 41015 deletions

View File

@@ -11,6 +11,7 @@
<modules>
<module>ruoyi-common-bom</module>
<module>ruoyi-common-social</module>
<module>ruoyi-common-core</module>
<module>ruoyi-common-doc</module>
<module>ruoyi-common-excel</module>

View File

@@ -14,7 +14,7 @@
</description>
<properties>
<revision>5.0.0</revision>
<revision>5.1.0-BETA</revision>
</properties>
<dependencyManagement>
@@ -117,6 +117,12 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-social</artifactId>
<version>${revision}</version>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
@@ -165,6 +171,7 @@
<artifactId>ruoyi-common-websocket</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -34,6 +34,11 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@@ -2,16 +2,14 @@ package org.dromara.common.core.config;
import cn.hutool.core.util.ArrayUtil;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
/**
* 异步配置
@@ -22,16 +20,12 @@ import java.util.concurrent.ScheduledExecutorService;
@AutoConfiguration
public class AsyncConfig implements AsyncConfigurer {
@Autowired
@Qualifier("scheduledExecutorService")
private ScheduledExecutorService scheduledExecutorService;
/**
* 自定义 @Async 注解使用系统线程池
*/
@Override
public Executor getAsyncExecutor() {
return scheduledExecutorService;
return SpringUtils.getBean("scheduledExecutorService");
}
/**

View File

@@ -22,18 +22,19 @@ public class ValidatorConfig {
*/
@Bean
public Validator validator(MessageSource messageSource) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
// 国际化
factoryBean.setValidationMessageSource(messageSource);
// 设置使用 HibernateValidator 校验器
factoryBean.setProviderClass(HibernateValidator.class);
Properties properties = new Properties();
// 设置 快速异常返回
properties.setProperty("hibernate.validator.fail_fast", "true");
factoryBean.setValidationProperties(properties);
// 加载配置
factoryBean.afterPropertiesSet();
return factoryBean.getValidator();
try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) {
// 国际化
factoryBean.setValidationMessageSource(messageSource);
// 设置使用 HibernateValidator 校验器
factoryBean.setProviderClass(HibernateValidator.class);
Properties properties = new Properties();
// 设置 快速异常返回
properties.setProperty("hibernate.validator.fail_fast", "true");
factoryBean.setValidationProperties(properties);
// 加载配置
factoryBean.afterPropertiesSet();
return factoryBean.getValidator();
}
}
}

View File

@@ -31,4 +31,9 @@ public interface GlobalConstants {
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
/**
* 三方认证 redis key
*/
String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
}

View File

@@ -1,7 +1,9 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.Email;
import org.dromara.common.core.constant.UserConstants;
import lombok.Data;
import org.dromara.common.core.validate.auth.*;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.NotBlank;
@@ -15,6 +17,28 @@ import jakarta.validation.constraints.NotBlank;
@Data
public class LoginBody {
/**
* 客户端id
*/
@NotBlank(message = "{auth.clientid.not.blank}")
private String clientId;
/**
* 客户端key
*/
private String clientKey;
/**
* 客户端秘钥
*/
private String clientSecret;
/**
* 授权类型
*/
@NotBlank(message = "{auth.grant.type.not.blank}")
private String grantType;
/**
* 租户ID
*/
@@ -24,15 +48,15 @@ public class LoginBody {
/**
* 用户名
*/
@NotBlank(message = "{user.username.not.blank}")
@Length(min = UserConstants.USERNAME_MIN_LENGTH, max = UserConstants.USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
@NotBlank(message = "{user.username.not.blank}", groups = {PasswordGroup.class})
@Length(min = UserConstants.USERNAME_MIN_LENGTH, max = UserConstants.USERNAME_MAX_LENGTH, message = "{user.username.length.valid}", groups = {PasswordGroup.class})
private String username;
/**
* 用户密码
*/
@NotBlank(message = "{user.password.not.blank}")
@Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
@NotBlank(message = "{user.password.not.blank}", groups = {PasswordGroup.class})
@Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}", groups = {PasswordGroup.class})
private String password;
/**
@@ -45,4 +69,52 @@ public class LoginBody {
*/
private String uuid;
/**
* 手机号
*/
@NotBlank(message = "{user.phonenumber.not.blank}", groups = {SmsGroup.class})
private String phonenumber;
/**
* 短信code
*/
@NotBlank(message = "{sms.code.not.blank}", groups = {SmsGroup.class})
private String smsCode;
/**
* 邮箱
*/
@NotBlank(message = "{user.email.not.blank}", groups = {EmailGroup.class})
@Email(message = "{user.email.not.valid}")
private String email;
/**
* 邮箱code
*/
@NotBlank(message = "{email.code.not.blank}", groups = {EmailGroup.class})
private String emailCode;
/**
* 小程序code
*/
@NotBlank(message = "{xcx.code.not.blank}", groups = {WechatGroup.class})
private String xcxCode;
/**
* 第三方登录平台
*/
@NotBlank(message = "{social.source.not.blank}" , groups = {SocialGroup.class})
private String source;
/**
* 第三方登录code
*/
@NotBlank(message = "{social.code.not.blank}" , groups = {SocialGroup.class})
private String socialCode;
/**
* 第三方登录socialState
*/
@NotBlank(message = "{social.state.not.blank}" , groups = {SocialGroup.class})
private String socialState;
}

View File

@@ -0,0 +1,21 @@
package org.dromara.common.core.domain.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 第三方登录用户身份权限
*
* @author thiszhc is 三三
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class SocialLogin extends LoginUser{
/**
* openid
*/
private String openid;
}

View File

@@ -26,7 +26,12 @@ public enum DeviceType {
/**
* 小程序端
*/
XCX("xcx");
XCX("xcx"),
/**
* social第三方端
*/
SOCIAL("social");
private final String device;
}

View File

@@ -1,5 +1,10 @@
package org.dromara.common.core.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
@@ -7,6 +12,10 @@ import java.io.Serial;
*
* @author ruoyi
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
@Serial
@@ -22,12 +31,6 @@ public class GlobalException extends RuntimeException {
*/
private String detailMessage;
/**
* 空构造方法,避免反序列化问题
*/
public GlobalException() {
}
public GlobalException(String message) {
this.message = message;
}

View File

@@ -1,5 +1,10 @@
package org.dromara.common.core.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
@@ -7,6 +12,10 @@ import java.io.Serial;
*
* @author ruoyi
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public final class ServiceException extends RuntimeException {
@Serial
@@ -27,12 +36,6 @@ public final class ServiceException extends RuntimeException {
*/
private String detailMessage;
/**
* 空构造方法,避免反序列化问题
*/
public ServiceException() {
}
public ServiceException(String message) {
this.message = message;
}

View File

@@ -1,5 +1,6 @@
package org.dromara.common.core.exception.base;
import lombok.AllArgsConstructor;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import lombok.Data;
@@ -16,6 +17,7 @@ import java.io.Serial;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class BaseException extends RuntimeException {
@Serial
@@ -41,13 +43,6 @@ public class BaseException extends RuntimeException {
*/
private String defaultMessage;
public BaseException(String module, String code, Object[] args, String defaultMessage) {
this.module = module;
this.code = code;
this.args = args;
this.defaultMessage = defaultMessage;
}
public BaseException(String module, String code, Object[] args) {
this(module, code, args, null);
}

View File

@@ -0,0 +1,31 @@
package org.dromara.common.core.factory;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.DefaultPropertySourceFactory;
import org.springframework.core.io.support.EncodedResource;
import java.io.IOException;
/**
* yml 配置源工厂
*
* @author Lion Li
*/
public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
String sourceName = resource.getResource().getFilename();
if (StringUtils.isNotBlank(sourceName) && StringUtils.endsWithAny(sourceName, ".yml", ".yaml")) {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
factory.afterPropertiesSet();
return new PropertiesPropertySource(sourceName, factory.getObject());
}
return super.createPropertySource(name, resource);
}
}

View File

@@ -1,5 +1,7 @@
package org.dromara.common.core.service;
import java.util.Map;
/**
* 通用 字典服务
*
@@ -54,4 +56,12 @@ public interface DictService {
*/
String getDictValue(String dictType, String dictLabel, String separator);
/**
* 获取字典下所有的字典值与标签
*
* @param dictType 字典类型
* @return dictValue为keydictLabel为值组成的Map
*/
Map<String, String> getAllDictByDictType(String dictType);
}

View File

@@ -3,6 +3,7 @@ package org.dromara.common.core.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
/**
@@ -23,6 +24,10 @@ public class MessageUtils {
* @return 获取国际化翻译值
*/
public static String message(String code, Object... args) {
return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
try {
return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
} catch (NoSuchMessageException e) {
return code;
}
}
}

View File

@@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpSession;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -19,6 +20,7 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@@ -101,14 +103,22 @@ public class ServletUtils extends JakartaServletUtil {
* 获取request
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
try {
return getRequestAttributes().getRequest();
} catch (Exception e) {
return null;
}
}
/**
* 获取response
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
try {
return getRequestAttributes().getResponse();
} catch (Exception e) {
return null;
}
}
/**
@@ -119,8 +129,33 @@ public class ServletUtils extends JakartaServletUtil {
}
public static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
} catch (Exception e) {
return null;
}
}
public static String getHeader(HttpServletRequest request, String name) {
String value = request.getHeader(name);
if (StringUtils.isEmpty(value)) {
return StringUtils.EMPTY;
}
return urlDecode(value);
}
public static Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
}
return map;
}
/**

View File

@@ -72,7 +72,7 @@ public class StreamUtils {
return CollUtil.newArrayList();
}
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
return collection.stream().sorted(comparing).collect(Collectors.toList());
return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
}
/**
@@ -89,7 +89,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
}
/**
@@ -108,7 +108,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l));
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
}
/**
@@ -126,7 +126,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
.stream()
.stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
}
@@ -147,7 +147,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
.stream()
.stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
}
@@ -168,7 +168,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
.stream()
.stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
}

View File

@@ -0,0 +1,7 @@
package org.dromara.common.core.validate.auth;
/**
* @Author Michelle.Chung
*/
public interface EmailGroup {
}

View File

@@ -0,0 +1,7 @@
package org.dromara.common.core.validate.auth;
/**
* @Author Michelle.Chung
*/
public interface PasswordGroup {
}

View File

@@ -0,0 +1,7 @@
package org.dromara.common.core.validate.auth;
/**
* @Author Michelle.Chung
*/
public interface SmsGroup {
}

View File

@@ -0,0 +1,4 @@
package org.dromara.common.core.validate.auth;
public interface SocialGroup {
}

View File

@@ -0,0 +1,7 @@
package org.dromara.common.core.validate.auth;
/**
* @Author Michelle.Chung
*/
public interface WechatGroup {
}

View File

@@ -1,13 +1,13 @@
package org.dromara.common.doc.config;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.doc.config.properties.SwaggerProperties;
import org.dromara.common.doc.handler.OpenApiHandler;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.doc.config.properties.SpringDocProperties;
import org.dromara.common.doc.handler.OpenApiHandler;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.OpenApiCustomizer;
@@ -36,26 +36,26 @@ import java.util.Set;
*/
@RequiredArgsConstructor
@AutoConfiguration(before = SpringDocConfiguration.class)
@EnableConfigurationProperties(SwaggerProperties.class)
@EnableConfigurationProperties(SpringDocProperties.class)
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
public class SwaggerConfig {
public class SpringDocConfig {
private final ServerProperties serverProperties;
@Bean
@ConditionalOnMissingBean(OpenAPI.class)
public OpenAPI openApi(SwaggerProperties swaggerProperties) {
public OpenAPI openApi(SpringDocProperties properties) {
OpenAPI openApi = new OpenAPI();
// 文档基本信息
SwaggerProperties.InfoProperties infoProperties = swaggerProperties.getInfo();
SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
Info info = convertInfo(infoProperties);
openApi.info(info);
// 扩展文档信息
openApi.externalDocs(swaggerProperties.getExternalDocs());
openApi.tags(swaggerProperties.getTags());
openApi.paths(swaggerProperties.getPaths());
openApi.components(swaggerProperties.getComponents());
Set<String> keySet = swaggerProperties.getComponents().getSecuritySchemes().keySet();
openApi.externalDocs(properties.getExternalDocs());
openApi.tags(properties.getTags());
openApi.paths(properties.getPaths());
openApi.components(properties.getComponents());
Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
List<SecurityRequirement> list = new ArrayList<>();
SecurityRequirement securityRequirement = new SecurityRequirement();
keySet.forEach(securityRequirement::addList);
@@ -65,7 +65,7 @@ public class SwaggerConfig {
return openApi;
}
private Info convertInfo(SwaggerProperties.InfoProperties infoProperties) {
private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
Info info = new Info();
info.setTitle(infoProperties.getTitle());
info.setDescription(infoProperties.getDescription());

View File

@@ -18,8 +18,8 @@ import java.util.List;
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperties {
@ConfigurationProperties(prefix = "springdoc")
public class SpringDocProperties {
/**
* 文档基本信息

View File

@@ -1 +1 @@
org.dromara.common.doc.config.SwaggerConfig
org.dromara.common.doc.config.SpringDocConfig

View File

@@ -32,7 +32,7 @@ public @interface EncryptField {
String publicKey() default "";
/**
* 钥。RSA、SM2需要
* 钥。RSA、SM2需要
*/
String privateKey() default "";

View File

@@ -0,0 +1,32 @@
package org.dromara.common.encrypt.config;
import jakarta.servlet.DispatcherType;
import org.dromara.common.encrypt.filter.CryptoFilter;
import org.dromara.common.encrypt.properties.ApiDecryptProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
/**
* api 解密自动配置
*
* @author wdhcr
*/
@AutoConfiguration
@EnableConfigurationProperties(ApiDecryptProperties.class)
@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
public class ApiDecryptAutoConfiguration {
@Bean
public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new CryptoFilter(properties));
registration.addUrlPatterns("/*");
registration.setName("cryptoFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registration;
}
}

View File

@@ -0,0 +1,49 @@
package org.dromara.common.encrypt.filter;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.properties.ApiDecryptProperties;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import java.io.IOException;
/**
* Crypto 过滤器
*
* @author wdhcr
*/
public class CryptoFilter implements Filter {
private final ApiDecryptProperties properties;
public CryptoFilter(ApiDecryptProperties properties) {
this.properties = properties;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
HttpServletRequest servletRequest = (HttpServletRequest) request;
// 是否为 json 请求
if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
// 是否为 put 或者 post 请求
if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
// 是否存在加密标头
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
if (StringUtils.isNotBlank(headerValue)) {
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPublicKey(), properties.getPrivateKey(), properties.getHeaderFlag());
}
}
}
chain.doFilter(ObjectUtil.defaultIfNull(requestWrapper, request), response);
}
@Override
public void destroy() {
}
}

View File

@@ -0,0 +1,94 @@
package org.dromara.common.encrypt.filter;
import cn.hutool.core.io.IoUtil;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.encrypt.utils.EncryptUtils;
import org.springframework.http.MediaType;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 解密请求参数工具类
*
* @author wdhcr
*/
public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public DecryptRequestBodyWrapper(HttpServletRequest request, String publicKey, String privateKey, String headerFlag) throws IOException {
super(request);
// 获取 AES 密码 采用 RSA 加密
String headerRsa = request.getHeader(headerFlag);
String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
// 解密 AES 密码
String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
request.setCharacterEncoding(Constants.UTF8);
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 解密 body 采用 AES 加密
String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
body = decryptBody.getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public int getContentLength() {
return body.length;
}
@Override
public long getContentLengthLong() {
return body.length;
}
@Override
public String getContentType() {
return MediaType.APPLICATION_JSON_VALUE;
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public int available() {
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}

View File

@@ -1,6 +1,7 @@
package org.dromara.common.encrypt.interceptor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -75,7 +76,7 @@ public class MybatisDecryptInterceptor implements Interceptor {
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
try {
for (Field field : fields) {
field.set(sourceObject, this.decryptField(String.valueOf(field.get(sourceObject)), field));
field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field));
}
} catch (Exception e) {
log.error("处理解密字段时出错", e);

View File

@@ -1,6 +1,7 @@
package org.dromara.common.encrypt.interceptor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -84,7 +85,7 @@ public class MybatisEncryptInterceptor implements Interceptor {
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
try {
for (Field field : fields) {
field.set(sourceObject, this.encryptField(String.valueOf(field.get(sourceObject)), field));
field.set(sourceObject, this.encryptField(Convert.toStr(field.get(sourceObject)), field));
}
} catch (Exception e) {
log.error("处理加密字段时出错", e);

View File

@@ -0,0 +1,34 @@
package org.dromara.common.encrypt.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* api解密属性配置类
* @author wdhcr
*/
@Data
@ConfigurationProperties(prefix = "api-decrypt")
public class ApiDecryptProperties {
/**
* 加密开关
*/
private Boolean enabled;
/**
* 头部标识
*/
private String headerFlag;
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
}

View File

@@ -1 +1,3 @@
org.dromara.common.encrypt.config.EncryptorAutoConfiguration
org.dromara.common.encrypt.config.ApiDecryptAutoConfiguration

View File

@@ -37,14 +37,26 @@ public class ExcelEnumConvert implements Converter<Object> {
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
Object codeValue = cellData.getData();
cellData.checkEmpty();
// Excel中填入的是枚举中指定的描述
Object textValue = switch (cellData.getType()) {
case STRING, DIRECT_STRING, RICH_TEXT_STRING -> cellData.getStringValue();
case NUMBER -> cellData.getNumberValue();
case BOOLEAN -> cellData.getBooleanValue();
default -> throw new IllegalArgumentException("单元格类型异常!");
};
// 如果是空值
if (ObjectUtil.isNull(codeValue)) {
if (ObjectUtil.isNull(textValue)) {
return null;
}
Map<Object, String> enumValueMap = beforeConvert(contentProperty);
String textValue = enumValueMap.get(codeValue);
return Convert.convert(contentProperty.getField().getType(), textValue);
Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
// 从Java输出至Excel是code转text
// 因此从Excel转Java应该将text与code对调
Map<Object, Object> enumTextToCodeMap = new HashMap<>();
enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
// 应该从text -> code中查找
Object codeValue = enumTextToCodeMap.get(textValue);
return Convert.convert(contentProperty.getField().getType(), codeValue);
}
@Override

View File

@@ -0,0 +1,149 @@
package org.dromara.common.excel.core;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* <h1>Excel下拉可选项</h1>
* 注意为确保下拉框解析正确传值务必使用createOptionValue()做为值的拼接
*
* @author Emil.Zhang
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@SuppressWarnings("unused")
public class DropDownOptions {
/**
* 一级下拉所在列index从0开始算
*/
private int index = 0;
/**
* 二级下拉所在的index从0开始算不能与一级相同
*/
private int nextIndex = 0;
/**
* 一级下拉所包含的数据
*/
private List<String> options = new ArrayList<>();
/**
* 二级下拉所包含的数据Map
* <p>以每一个一级选项值为Key每个一级选项对应的二级数据为Value</p>
*/
private Map<String, List<String>> nextOptions = new HashMap<>();
/**
* 分隔符
*/
private static final String DELIMITER = "_";
/**
* 创建只有一级的下拉选
*/
public DropDownOptions(int index, List<String> options) {
this.index = index;
this.options = options;
}
/**
* <h2>创建每个选项可选值</h2>
* <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
*
* @param vars 可选值内包含的参数
* @return 合规的可选值
*/
public static String createOptionValue(Object... vars) {
StringBuilder stringBuffer = new StringBuilder();
String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
for (int i = 0; i < vars.length; i++) {
String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
if (!var.matches(regex)) {
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
}
stringBuffer.append(var);
if (i < vars.length - 1) {
// 直至最后一个前都以_作为切割线
stringBuffer.append(DELIMITER);
}
}
if (stringBuffer.toString().matches("^\\d_*$")) {
throw new ServiceException("禁止以数字开头");
}
return stringBuffer.toString();
}
/**
* 将处理后合理的可选值解析为原始的参数
*
* @param option 经过处理后的合理的可选项
* @return 原始的参数
*/
public static List<String> analyzeOptionValue(String option) {
return StrUtil.split(option, DELIMITER, true, true);
}
/**
* 创建级联下拉选项
*
* @param parentList 父实体可选项原始数据
* @param parentIndex 父下拉选位置
* @param sonList 子实体可选项原始数据
* @param sonIndex 子下拉选位置
* @param parentHowToGetIdFunction 父类如何获取唯一标识
* @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
* @param howToBuildEveryOption 如何生成下拉选内容
* @return 级联下拉选项
*/
public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
int parentIndex,
List<T> sonList,
int sonIndex,
Function<T, Number> parentHowToGetIdFunction,
Function<T, Number> sonHowToGetParentIdFunction,
Function<T, String> howToBuildEveryOption) {
DropDownOptions parentLinkSonOptions = new DropDownOptions();
// 先创建父类的下拉
parentLinkSonOptions.setIndex(parentIndex);
parentLinkSonOptions.setOptions(
parentList.stream()
.map(howToBuildEveryOption)
.collect(Collectors.toList())
);
// 提取父-子级联下拉
Map<String, List<String>> sonOptions = new HashMap<>();
// 父级依据自己的ID分组
Map<Number, List<T>> parentGroupByIdMap =
parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
// 遍历每个子集提取到Map中
sonList.forEach(everySon -> {
if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
// 找到对应的上级
T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
// 提取名称和ID作为Key
String key = howToBuildEveryOption.apply(parentObj);
// Key对应的Value
List<String> thisParentSonOptionList;
if (sonOptions.containsKey(key)) {
thisParentSonOptionList = sonOptions.get(key);
} else {
thisParentSonOptionList = new ArrayList<>();
sonOptions.put(key, thisParentSonOptionList);
}
// 往Value中添加当前子集选项
thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
}
});
parentLinkSonOptions.setNextIndex(sonIndex);
parentLinkSonOptions.setNextOptions(sonOptions);
return parentLinkSonOptions;
}
}

View File

@@ -0,0 +1,373 @@
package org.dromara.common.excel.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.ss.util.WorkbookUtil;
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.annotation.ExcelEnumFormat;
import java.lang.reflect.Field;
import java.util.*;
/**
* <h1>Excel表格下拉选操作</h1>
* 考虑到下拉选过多可能导致Excel打开缓慢的问题只校验前1000行
* <p>
* 即只有前1000行的数据可以用下拉框超出的自行通过限制数据量的形式第二次输出
*
* @author Emil.Zhang
*/
@Slf4j
public class ExcelDownHandler implements SheetWriteHandler {
/**
* Excel表格中的列名英文
* 仅为了解析列英文,禁止修改
*/
private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* 单选数据Sheet名
*/
private static final String OPTIONS_SHEET_NAME = "options";
/**
* 联动选择数据Sheet名的头
*/
private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
/**
* 下拉可选项
*/
private final List<DropDownOptions> dropDownOptions;
/**
* 当前单选进度
*/
private int currentOptionsColumnIndex;
/**
* 当前联动选择进度
*/
private int currentLinkedOptionsSheetIndex;
private final DictService dictService;
public ExcelDownHandler(List<DropDownOptions> options) {
this.dropDownOptions = options;
this.currentOptionsColumnIndex = 0;
this.currentLinkedOptionsSheetIndex = 0;
this.dictService = SpringUtils.getBean(DictService.class);
}
/**
* <h2>开始创建下拉数据</h2>
* 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
* 如果有且设置了value值则将其直接置为下拉可选项
* <p>
* 2.或者在调用ExcelUtil时指定了可选项将依据传入的可选项做下拉
* <p>
* 3.二者并存,注意调用方式
*/
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Sheet sheet = writeSheetHolder.getSheet();
// 开始设置下拉框 HSSFWorkbook
DataValidationHelper helper = sheet.getDataValidationHelper();
Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
Workbook workbook = writeWorkbookHolder.getWorkbook();
int length = fields.length;
for (int i = 0; i < length; i++) {
// 循环实体中的每个属性
// 可选的下拉值
List<String> options = new ArrayList<>();
if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
// 如果指定了@ExcelDictFormat则使用字典的逻辑
ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
String dictType = format.dictType();
String converterExp = format.readConverterExp();
if (StrUtil.isNotBlank(dictType)) {
// 如果传递了字典名,则依据字典建立下拉
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
.values();
options = new ArrayList<>(values);
} else if (StrUtil.isNotBlank(converterExp)) {
// 如果指定了确切的值,则直接解析确切的值
options = StrUtil.split(converterExp, format.separator(), true, true);
}
} else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
// 否则如果指定了@ExcelEnumFormat则使用枚举的逻辑
ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
options = StreamUtils.toList(values, String::valueOf);
}
if (ObjectUtil.isNotEmpty(options)) {
// 仅当下拉可选项不为空时执行
// 获取列下标,默认为当前循环次数
int index = i;
if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
// 如果指定了列下标,以指定的为主
index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
}
if (options.size() > 20) {
// 这里限制如果可选项大于20则使用额外表形式
dropDownWithSheet(helper, workbook, sheet, index, options);
} else {
// 否则使用固定值形式
dropDownWithSimple(helper, sheet, index, options);
}
}
}
if (CollUtil.isEmpty(dropDownOptions)) {
return;
}
dropDownOptions.forEach(everyOptions -> {
// 如果传递了下拉框选择器参数
if (!everyOptions.getNextOptions().isEmpty()) {
// 当二级选项不为空时,使用额外关联表的形式
dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
} else if (everyOptions.getOptions().size() > 10) {
// 当一级选项参数个数大于10使用额外表的形式
dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
} else if (everyOptions.getOptions().size() != 0) {
// 当一级选项个数不为空,使用默认形式
dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
}
});
}
/**
* <h2>简单下拉框</h2>
* 直接将可选项拼接为指定列的数据校验值
*
* @param celIndex 列index
* @param value 下拉选可选值
*/
private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
if (ObjectUtil.isEmpty(value)) {
return;
}
this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
}
/**
* <h2>额外表格形式的级联下拉框</h2>
*
* @param options 额外表格形式存储的下拉可选项
*/
private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
// 创建联动下拉数据表
Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
// 将下拉表隐藏
workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
// 完善横向的一级选项数据表
List<String> firstOptions = options.getOptions();
Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
// 创建名称管理器
Name name = workbook.createName();
// 设置名称管理器的别名
name.setNameName(linkedOptionsSheetName);
// 以横向第一行创建一级下拉拼接引用位置
String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
linkedOptionsSheetName,
getExcelColumnName(0),
getExcelColumnName(firstOptions.size())
);
// 设置名称管理器的引用位置
name.setRefersToFormula(firstOptionsFunction);
// 设置数据校验为序列模式,引用的是名称管理器中的别名
this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
// 先提取主表中一级下拉的列名
String firstOptionsColumnName = getExcelColumnName(columIndex);
// 一次循环是每一个一级选项
int finalI = columIndex;
// 本次循环的一级选项值
String thisFirstOptionsValue = firstOptions.get(columIndex);
// 创建第一行的数据
Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
// 如果不存在则创建第一行
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
// 第一行当前列
.createCell(columIndex)
// 设置值为当前一级选项值
.setCellValue(thisFirstOptionsValue);
// 第二行开始,设置第二级别选项参数
List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
if (CollUtil.isEmpty(secondOptions)) {
// 必须保证至少有一个关联选项否则将导致Excel解析错误
secondOptions = Collections.singletonList("暂无_0");
}
// 以该一级选项值创建子名称管理器
Name sonName = workbook.createName();
// 设置名称管理器的别名
sonName.setNameName(thisFirstOptionsValue);
// 以第二行该列数据拼接引用位置
String sonFunction = String.format("%s!$%s$2:$%s$%d",
linkedOptionsSheetName,
firstOptionsColumnName,
firstOptionsColumnName,
secondOptions.size() + 1
);
// 设置名称管理器的引用位置
sonName.setRefersToFormula(sonFunction);
// 数据验证为序列模式,引用到每一个主表中的二级选项位置
// 创建子项的名称管理器只是为了使得Excel可以识别到数据
String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
for (int i = 0; i < 100; i++) {
// 以一级选项对应的主体所在位置创建二级下拉
String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
// 二级只能主表每一行的每一列添加二级校验
markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
}
for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
// 从第二行开始填充二级选项
int finalRowIndex = rowIndex + 1;
int finalColumIndex = columIndex;
Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
// 没有则创建
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
Optional
// 在本级一级选项所在的列
.ofNullable(row.getCell(finalColumIndex))
// 不存在则创建
.orElseGet(() -> row.createCell(finalColumIndex))
// 设置二级选项值
.setCellValue(secondOptions.get(rowIndex));
}
}
currentLinkedOptionsSheetIndex++;
}
/**
* <h2>额外表格形式的普通下拉框</h2>
* 由于下拉框可选值数量过多为提升Excel打开效率使用额外表格形式做下拉
*
* @param celIndex 下拉选
* @param value 下拉选可选值
*/
private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
// 创建下拉数据表
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
// 将下拉表隐藏
workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
// 完善纵向的一级选项数据表
for (int i = 0; i < value.size(); i++) {
int finalI = i;
// 获取每一选项行,如果没有则创建
Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
.orElseGet(() -> simpleDataSheet.createRow(finalI));
// 获取本级选项对应的选项列,如果没有则创建
Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
.orElseGet(() -> row.createCell(currentOptionsColumnIndex));
// 设置值
cell.setCellValue(value.get(i));
}
// 创建名称管理器
Name name = workbook.createName();
// 设置名称管理器的别名
String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
name.setNameName(nameName);
// 以纵向第一列创建一级下拉拼接引用位置
String function = String.format("%s!$%s$1:$%s$%d",
OPTIONS_SHEET_NAME,
getExcelColumnName(currentOptionsColumnIndex),
getExcelColumnName(currentOptionsColumnIndex),
value.size());
// 设置名称管理器的引用位置
name.setRefersToFormula(function);
// 设置数据校验为序列模式,引用的是名称管理器中的别名
this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
currentOptionsColumnIndex++;
}
/**
* 挂载下拉的列,仅限一级选项
*/
private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
DataValidationConstraint constraint) {
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
markDataValidationToSheet(helper, sheet, constraint, addressList);
}
/**
* 挂载下拉的列,仅限二级选项
*/
private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
Integer celIndex, DataValidationConstraint constraint) {
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
markDataValidationToSheet(helper, sheet, constraint, addressList);
}
/**
* 应用数据校验
*/
private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
DataValidationConstraint constraint, CellRangeAddressList addressList) {
// 数据有效性对象
DataValidation dataValidation = helper.createValidation(constraint, addressList);
// 处理Excel兼容性问题
if (dataValidation instanceof XSSFDataValidation) {
//数据校验
dataValidation.setSuppressDropDownArrow(true);
//错误提示
dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
dataValidation.setShowErrorBox(true);
//选定提示
dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
dataValidation.setShowPromptBox(true);
sheet.addValidationData(dataValidation);
} else {
dataValidation.setSuppressDropDownArrow(false);
}
sheet.addValidationData(dataValidation);
}
/**
* <h2>依据列index获取列名英文</h2>
* 依据列index转换为Excel中的列名英文
* <p>例如第1列index为0解析出来为A列</p>
* 第27列index为26解析为AA列
* <p>第28列index为27解析为AB列</p>
*
* @param columnIndex 列index
* @return 列index所在得英文名
*/
private String getExcelColumnName(int columnIndex) {
// 26一循环的次数
int columnCircleCount = columnIndex / 26;
// 26一循环内的位置
int thisCircleColumnIndex = columnIndex % 26;
// 26一循环的次数大于0则视为栏名至少两位
String columnPrefix = columnCircleCount == 0
? StrUtil.EMPTY
: StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
// 从26一循环内取对应的栏位名
String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
// 将二者拼接即为最终的栏位名
return columnPrefix + columnNext;
}
}

View File

@@ -10,21 +10,19 @@ import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.convert.ExcelBigNumberConvert;
import org.dromara.common.excel.core.CellMergeStrategy;
import org.dromara.common.excel.core.DefaultExcelListener;
import org.dromara.common.excel.core.ExcelListener;
import org.dromara.common.excel.core.ExcelResult;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.convert.ExcelBigNumberConvert;
import org.dromara.common.excel.core.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -87,7 +85,26 @@ public class ExcelUtil {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
exportExcel(list, sheetName, clazz, false, os);
exportExcel(list, sheetName, clazz, false, os, null);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
}
/**
* 导出excel
*
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @param clazz 实体类
* @param response 响应体
* @param options 级联下拉选
*/
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
exportExcel(list, sheetName, clazz, false, os, options);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
@@ -106,7 +123,27 @@ public class ExcelUtil {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
exportExcel(list, sheetName, clazz, merge, os);
exportExcel(list, sheetName, clazz, merge, os, null);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
}
/**
* 导出excel
*
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @param clazz 实体类
* @param merge 是否合并单元格
* @param response 响应体
* @param options 级联下拉选
*/
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
exportExcel(list, sheetName, clazz, merge, os, options);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
@@ -121,7 +158,20 @@ public class ExcelUtil {
* @param os 输出流
*/
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
exportExcel(list, sheetName, clazz, false, os);
exportExcel(list, sheetName, clazz, false, os, null);
}
/**
* 导出excel
*
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @param clazz 实体类
* @param os 输出流
* @param options 级联下拉选内容
*/
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
exportExcel(list, sheetName, clazz, false, os, options);
}
/**
@@ -133,7 +183,8 @@ public class ExcelUtil {
* @param merge 是否合并单元格
* @param os 输出流
*/
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, OutputStream os) {
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
OutputStream os, List<DropDownOptions> options) {
ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
.autoCloseStream(false)
// 自动适配
@@ -145,6 +196,8 @@ public class ExcelUtil {
// 合并处理器
builder.registerWriteHandler(new CellMergeStrategy(list, true));
}
// 添加下拉框操作
builder.registerWriteHandler(new ExcelDownHandler(options));
builder.doWrite(list);
}
@@ -253,7 +306,7 @@ public class ExcelUtil {
/**
* 重置响应体
*/
private static void resetResponse(String sheetName, HttpServletResponse response) {
private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
String filename = encodingFilename(sheetName);
FileUtils.setAttachmentResponseHeader(response, filename);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
@@ -275,7 +328,7 @@ public class ExcelUtil {
if (StringUtils.containsAny(propertyValue, separator)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[0].equals(value)) {
propertyString.append(itemArray[1]).append(separator);
propertyString.append(itemArray[1] + separator);
break;
}
}
@@ -304,7 +357,7 @@ public class ExcelUtil {
if (StringUtils.containsAny(propertyValue, separator)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[1].equals(value)) {
propertyString.append(itemArray[0]).append(separator);
propertyString.append(itemArray[0] + separator);
break;
}
}

View File

@@ -1,6 +1,7 @@
package org.dromara.common.idempotent.aspectj;
import cn.dev33.satoken.SaManager;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import org.dromara.common.core.constant.GlobalConstants;
@@ -25,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
/**
* 防止重复提交(参考美团GTIS防重系统)
@@ -39,10 +41,8 @@ public class RepeatSubmitAspect {
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
// 如果注解不为0 则使用注解数值
long interval = 0;
if (repeatSubmit.interval() > 0) {
interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
}
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
if (interval < 1000) {
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
}
@@ -58,9 +58,7 @@ public class RepeatSubmitAspect {
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
// 唯一标识指定key + url + 消息头)
String cacheRepeatKey = GlobalConstants.REPEAT_SUBMIT_KEY + url + submitKey;
String key = RedisUtils.getCacheObject(cacheRepeatKey);
if (key == null) {
RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));
if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
String message = repeatSubmit.message();
@@ -78,7 +76,7 @@ public class RepeatSubmitAspect {
*/
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
if (jsonResult instanceof R r) {
if (jsonResult instanceof R<?> r) {
try {
// 成功则不删除redis数据 保证在有效时间内无法重复提交
if (r.getCode() == R.SUCCESS) {
@@ -107,19 +105,16 @@ public class RepeatSubmitAspect {
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
StringBuilder params = new StringBuilder();
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
try {
params.append(JsonUtils.toJsonString(o)).append(" ");
} catch (Exception e) {
e.printStackTrace();
}
}
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
params.add(JsonUtils.toJsonString(o));
}
}
return params.toString().trim();
return params.toString();
}
/**
@@ -140,13 +135,12 @@ public class RepeatSubmitAspect {
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
|| o instanceof BindingResult;
}
}

View File

@@ -15,17 +15,21 @@
ruoyi-common-job 定时任务
</description>
<dependencies>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- xxl-job-core -->
<!--PowerJob-->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-worker-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-official-processors</artifactId>
</dependency>
<dependency>

View File

@@ -1,80 +0,0 @@
package com.xxl.job.core.glue.impl;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import com.xxl.job.core.glue.GlueFactory;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
/**
* @author xuxueli 2018-11-01
*/
public class SpringGlueFactory extends GlueFactory {
private static Logger logger = LoggerFactory.getLogger(SpringGlueFactory.class);
/**
* inject action of spring
* @param instance
*/
@Override
public void injectService(Object instance){
if (instance==null) {
return;
}
if (XxlJobSpringExecutor.getApplicationContext() == null) {
return;
}
Field[] fields = instance.getClass().getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
Object fieldBean = null;
// with bean-id, bean could be found by both @Resource and @Autowired, or bean could only be found by @Autowired
if (AnnotationUtils.getAnnotation(field, Resource.class) != null) {
try {
Resource resource = AnnotationUtils.getAnnotation(field, Resource.class);
if (resource.name()!=null && resource.name().length()>0){
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(resource.name());
} else {
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getName());
}
} catch (Exception e) {
}
if (fieldBean==null ) {
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getType());
}
} else if (AnnotationUtils.getAnnotation(field, Autowired.class) != null) {
Qualifier qualifier = AnnotationUtils.getAnnotation(field, Qualifier.class);
if (qualifier!=null && qualifier.value()!=null && qualifier.value().length()>0) {
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(qualifier.value());
} else {
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getType());
}
}
if (fieldBean!=null) {
field.setAccessible(true);
try {
field.set(instance, fieldBean);
} catch (IllegalArgumentException e) {
logger.error(e.getMessage(), e);
} catch (IllegalAccessException e) {
logger.error(e.getMessage(), e);
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
package org.dromara.common.job.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import tech.powerjob.worker.PowerJobWorker;
/**
* 启动定时任务
* @author yhan219
* @since 2023/6/2
*/
@Configuration
@ConditionalOnBean(PowerJobWorker.class)
@ConditionalOnProperty(prefix = "powerjob.worker", name = "enabled", havingValue = "true")
@EnableScheduling
public class PowerJobConfig {
}

View File

@@ -1,38 +0,0 @@
package org.dromara.common.job.config;
import org.dromara.common.job.config.properties.XxlJobProperties;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* xxl-job config
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(XxlJobProperties.class)
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true")
public class XxlJobConfig {
@Bean
public XxlJobSpringExecutor xxlJobExecutor(XxlJobProperties xxlJobProperties) {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdminAddresses());
xxlJobSpringExecutor.setAccessToken(xxlJobProperties.getAccessToken());
XxlJobProperties.Executor executor = xxlJobProperties.getExecutor();
xxlJobSpringExecutor.setAppname(executor.getAppname());
xxlJobSpringExecutor.setAddress(executor.getAddress());
xxlJobSpringExecutor.setIp(executor.getIp());
xxlJobSpringExecutor.setPort(executor.getPort());
xxlJobSpringExecutor.setLogPath(executor.getLogPath());
xxlJobSpringExecutor.setLogRetentionDays(executor.getLogRetentionDays());
return xxlJobSpringExecutor;
}
}

View File

@@ -1,40 +0,0 @@
package org.dromara.common.job.config.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* xxljob配置类
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "xxl.job")
public class XxlJobProperties {
private Boolean enabled;
private String adminAddresses;
private String accessToken;
private Executor executor;
@Data
@NoArgsConstructor
public static class Executor {
private String appname;
private String address;
private String ip;
private int port;
private String logPath;
private int logRetentionDays;
}
}

View File

@@ -2,6 +2,7 @@ package org.dromara.common.log.aspect;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.dromara.common.core.utils.ServletUtils;
@@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
/**
* 操作日志记录处理
@@ -170,26 +172,23 @@ public class LogAspect {
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
StringBuilder params = new StringBuilder();
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
try {
String str = JsonUtils.toJsonString(o);
Dict dict = JsonUtils.parseMap(str);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
MapUtil.removeAny(dict, excludeParamNames);
str = JsonUtils.toJsonString(dict);
}
params.append(str).append(" ");
} catch (Exception e) {
e.printStackTrace();
}
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
String str = JsonUtils.toJsonString(o);
Dict dict = JsonUtils.parseMap(str);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
MapUtil.removeAny(dict, excludeParamNames);
str = JsonUtils.toJsonString(dict);
}
params.add(str);
}
}
return params.toString().trim();
return params.toString();
}
/**
@@ -210,12 +209,11 @@ public class LogAspect {
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
|| o instanceof BindingResult;
}
}

View File

@@ -29,7 +29,7 @@
<!-- dynamic-datasource 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
</dependency>
<dependency>

View File

@@ -1,45 +0,0 @@
/*
* Copyright © 2018 organization baomidou
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.dynamic.datasource.processor.jakarta;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import jakarta.servlet.http.HttpServletRequest;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author TaoYu
* @since 3.6.0
*/
public class DsJakartaHeaderProcessor extends DsProcessor {
/**
* header prefix
*/
private static final String HEADER_PREFIX = "#header";
@Override
public boolean matches(String key) {
return key.startsWith(HEADER_PREFIX);
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getHeader(key.substring(8));
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright © 2018 organization baomidou
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.dynamic.datasource.processor.jakarta;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import jakarta.servlet.http.HttpServletRequest;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author TaoYu
* @since 3.6.0
*/
public class DsJakartaSessionProcessor extends DsProcessor {
/**
* session开头
*/
private static final String SESSION_PREFIX = "#session";
@Override
public boolean matches(String key) {
return key.startsWith(SESSION_PREFIX);
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getSession().getAttribute(key.substring(9)).toString();
}
}

View File

@@ -7,11 +7,13 @@ import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
@@ -22,6 +24,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement(proxyTargetClass = true)
@AutoConfiguration
@MapperScan("${mybatis-plus.mapperPackage}")
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
public class MybatisPlusConfig {
@Bean

View File

@@ -12,6 +12,9 @@ import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 数据库助手
@@ -69,4 +72,11 @@ public class DataBaseHelper {
// find_in_set(100 , '0,100,101')
return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
}
/**
* 获取当前加载的数据库名
*/
public static List<String> getDataSourceNameList() {
return new ArrayList<>(DS.getDataSources().keySet());
}
}

View File

@@ -0,0 +1,33 @@
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
# MyBatisPlus配置
# https://baomidou.com/config/
mybatis-plus:
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
checkConfigLocation: false
configuration:
# 自动驼峰命名规则camel case映射
mapUnderscoreToCamelCase: true
# MyBatis 自动映射策略
# NONE不启用 PARTIAL只对非嵌套 resultMap 自动映射 FULL对所有 resultMap 自动映射
autoMappingBehavior: FULL
# MyBatis 自动映射时未知列或未知属性处理策
# NONE不做处理 WARNING打印相关警告 FAILING抛出异常和详细信息
autoMappingUnknownColumnBehavior: NONE
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
# 是否打印 Logo banner
banner: true
dbConfig:
# 主键类型
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
idType: ASSIGN_ID
# 逻辑已删除值(框架表均使用此值 禁止随意修改)
logicDeleteValue: 2
# 逻辑未删除值
logicNotDeleteValue: 0
insertStrategy: NOT_NULL
updateStrategy: NOT_NULL
whereStrategy: NOT_NULL

View File

@@ -24,6 +24,7 @@ import org.dromara.common.oss.exception.OssException;
import org.dromara.common.oss.properties.OssProperties;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
@@ -115,6 +116,18 @@ public class OssClient {
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public UploadResult upload(File file, String path) {
try {
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
// 设置上传对象的 Acl 为公共读
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public void delete(String path) {
path = path.replace(getUrl() + "/", "");
try {
@@ -132,6 +145,10 @@ public class OssClient {
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
}
public UploadResult uploadSuffix(File file, String suffix) {
return upload(file, getPath(properties.getPrefix(), suffix));
}
/**
* 获取文件元数据
*

View File

@@ -51,13 +51,13 @@ public class OssFactory {
if (client == null) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("创建OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(configKey);
return CLIENT_CACHE.get(key);
}
// 配置不相同则重新构建
if (!client.checkPropertiesSame(properties)) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("重载OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(configKey);
return CLIENT_CACHE.get(key);
}
return client;
}

View File

@@ -118,6 +118,10 @@ public class PlusSpringCacheManager implements CacheManager {
@Override
public Cache getCache(String name) {
// 重写 cacheName 支持多参数
String[] array = StringUtils.delimitedListToStringArray(name, "#");
name = array[0];
Cache cache = instanceMap.get(name);
if (cache != null) {
return cache;
@@ -132,9 +136,6 @@ public class PlusSpringCacheManager implements CacheManager {
configMap.put(name, config);
}
// 重写 cacheName 支持多参数
String[] array = StringUtils.delimitedListToStringArray(name, "#");
name = array[0];
if (array.length > 1) {
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
}

View File

@@ -1,8 +1,8 @@
package org.dromara.common.redis.utils;
import org.dromara.common.core.utils.SpringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
import org.redisson.api.*;
import java.time.Duration;
@@ -129,6 +129,18 @@ public class RedisUtils {
batch.execute();
}
/**
* 如果不存在则设置 并返回 true 如果存在则返回 false
*
* @param key 缓存的键值
* @param value 缓存的值
* @return set成功或失败
*/
public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
RBucket<T> bucket = CLIENT.getBucket(key);
return bucket.setIfAbsent(value, duration);
}
/**
* 注册对象监听器
* <p>
@@ -374,6 +386,21 @@ public class RedisUtils {
return rMap.remove(hKey);
}
/**
* 删除Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键
*/
public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
RBatch batch = CLIENT.createBatch();
RMapAsync<String, T> rMap = batch.getMap(key);
for (String hKey : hKeys) {
rMap.removeAsync(hKey);
}
batch.execute();
}
/**
* 获取多个Hash中的数据
*

View File

@@ -4,10 +4,12 @@ import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.dromara.common.satoken.core.dao.PlusSaTokenDao;
import org.dromara.common.satoken.core.service.SaPermissionImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
@@ -16,6 +18,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
* @author Lion Li
*/
@AutoConfiguration
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
public class SaTokenConfig implements WebMvcConfigurer {
@Bean

View File

@@ -2,17 +2,17 @@ package org.dromara.common.satoken.utils;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.constant.UserConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.DeviceType;
import org.dromara.common.core.enums.UserType;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.Set;
@@ -34,31 +34,21 @@ public class LoginHelper {
public static final String LOGIN_USER_KEY = "loginUser";
public static final String TENANT_KEY = "tenantId";
public static final String USER_KEY = "userId";
/**
* 登录系统
*
* @param loginUser 登录用户信息
*/
public static void login(LoginUser loginUser) {
loginByDevice(loginUser, null);
}
public static final String CLIENT_KEY = "clientid";
/**
* 登录系统 基于 设备类型
* 针对相同用户体系不同设备
*
* @param loginUser 登录用户信息
* @param model 配置参数
*/
public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
public static void login(LoginUser loginUser, SaLoginModel model) {
SaStorage storage = SaHolder.getStorage();
storage.set(LOGIN_USER_KEY, loginUser);
storage.set(TENANT_KEY, loginUser.getTenantId());
storage.set(USER_KEY, loginUser.getUserId());
SaLoginModel model = new SaLoginModel();
if (ObjectUtil.isNotNull(deviceType)) {
model.setDevice(deviceType.getDevice());
}
model = ObjectUtil.defaultIfNull(model, new SaLoginModel());
StpUtil.login(loginUser.getLoginId(),
model.setExtra(TENANT_KEY, loginUser.getTenantId())
.setExtra(USER_KEY, loginUser.getUserId()));
@@ -73,7 +63,11 @@ public class LoginHelper {
if (loginUser != null) {
return loginUser;
}
loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
return null;
}
loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
return loginUser;
}
@@ -82,7 +76,11 @@ public class LoginHelper {
* 获取用户基于token
*/
public static LoginUser getLoginUser(String token) {
return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
SaSession session = StpUtil.getTokenSessionByToken(token);
if (ObjectUtil.isNull(session)) {
return null;
}
return (LoginUser) session.get(LOGIN_USER_KEY);
}
/**
@@ -137,8 +135,8 @@ public class LoginHelper {
* 获取用户类型
*/
public static UserType getUserType() {
String loginId = StpUtil.getLoginIdAsString();
return UserType.getUserType(loginId);
String loginType = StpUtil.getLoginIdAsString();
return UserType.getUserType(loginType);
}
/**

View File

@@ -0,0 +1,13 @@
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
# Sa-Token配置
sa-token:
# 允许动态设置 token 有效期
dynamic-active-timeout: true
# 允许从 请求参数 读取 token
is-read-body: true
# 允许从 header 读取 token
is-read-header: true
# 关闭 cookie 鉴权 从根源杜绝 csrf 漏洞风险
is-read-cookie: false
# token前缀
token-prefix: "Bearer"

View File

@@ -1,9 +1,13 @@
package org.dromara.common.security.config;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.security.config.properties.SecurityProperties;
import org.dromara.common.security.handler.AllUrlHandler;
import lombok.RequiredArgsConstructor;
@@ -44,6 +48,18 @@ public class SecurityConfig implements WebMvcConfigurer {
// 检查是否登录 是否有token
StpUtil.checkLogin();
// 检查 header 里的 clientId 与 token 里的是否一致
String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
if (!StringUtils.equals(headerCid, clientId)) {
// token 无效
throw NotLoginException.newInstance(
StpUtil.getLoginType(),
NotLoginException.INVALID_TOKEN,
NotLoginException.NOT_TOKEN_MESSAGE,
StpUtil.getTokenValue());
}
// 有效率影响 用于临时测试
// if (log.isDebugEnabled()) {
// log.debug("剩余有效时间: {}", StpUtil.getTokenTimeout());

View File

@@ -5,21 +5,22 @@ import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpStatus;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.DemoModeException;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
/**
* 全局异常处理器
@@ -81,6 +82,26 @@ public class GlobalExceptionHandler {
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
}
/**
* 请求路径中缺少必需的路径变量
*/
@ExceptionHandler(MissingPathVariableException.class)
public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
}
/**
* 请求参数类型不匹配
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
}
/**
* 拦截未知的运行时异常
*/

View File

@@ -16,22 +16,19 @@
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<optional>true</optional>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-spring-boot-starter</artifactId>
<exclusions>
<!-- 排除京东短信内存在的fastjson等待作者后续修复 -->
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -1,15 +1,6 @@
package org.dromara.common.sms.config;
import org.dromara.common.sms.config.properties.SmsProperties;
import org.dromara.common.sms.core.AliyunSmsTemplate;
import org.dromara.common.sms.core.SmsTemplate;
import org.dromara.common.sms.core.TencentSmsTemplate;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 短信配置类
@@ -18,31 +9,7 @@ import org.springframework.context.annotation.Configuration;
* @version 4.2.0
*/
@AutoConfiguration
@EnableConfigurationProperties(SmsProperties.class)
//@EnableConfigurationProperties(SmsProperties.class)
public class SmsConfig {
@Configuration
@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
@ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
static class AliyunSmsConfig {
@Bean
public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
return new AliyunSmsTemplate(smsProperties);
}
}
@Configuration
@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
@ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
static class TencentSmsConfig {
@Bean
public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
return new TencentSmsTemplate(smsProperties);
}
}
}

View File

@@ -1,45 +1,19 @@
package org.dromara.common.sms.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* SMS短信 配置属性
*
* @author Lion Li
* @version 4.2.0
*/
@Data
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
private Boolean enabled;
/**
* 配置节点
* 阿里云 dysmsapi.aliyuncs.com
* 腾讯云 sms.tencentcloudapi.com
*/
private String endpoint;
/**
* key
*/
private String accessKeyId;
/**
* 密匙
*/
private String accessKeySecret;
/*
* 短信签名
*/
private String signName;
/**
* 短信应用ID (腾讯专属)
*/
private String sdkAppId;
}
//package org.dromara.common.sms.config.properties;
//
//import lombok.Data;
//import org.springframework.boot.context.properties.ConfigurationProperties;
//
///**
// * SMS短信 配置属性
// *
// * @author Lion Li
// * @version 4.2.0
// */
//@Data
//@ConfigurationProperties(prefix = "sms")
//public class SmsProperties {
//
// private Boolean enabled;
//
//
//}

View File

@@ -1,66 +0,0 @@
package org.dromara.common.sms.core;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.sms.config.properties.SmsProperties;
import org.dromara.common.sms.entity.SmsResult;
import org.dromara.common.sms.exception.SmsException;
import lombok.SneakyThrows;
import java.util.Map;
/**
* Aliyun 短信模板
*
* @author Lion Li
* @version 4.2.0
*/
public class AliyunSmsTemplate implements SmsTemplate {
private SmsProperties properties;
private Client client;
@SneakyThrows(Exception.class)
public AliyunSmsTemplate(SmsProperties smsProperties) {
this.properties = smsProperties;
Config config = new Config()
// 您的AccessKey ID
.setAccessKeyId(smsProperties.getAccessKeyId())
// 您的AccessKey Secret
.setAccessKeySecret(smsProperties.getAccessKeySecret())
// 访问的域名
.setEndpoint(smsProperties.getEndpoint());
this.client = new Client(config);
}
@Override
public SmsResult send(String phones, String templateId, Map<String, String> param) {
if (StringUtils.isBlank(phones)) {
throw new SmsException("手机号不能为空");
}
if (StringUtils.isBlank(templateId)) {
throw new SmsException("模板ID不能为空");
}
SendSmsRequest req = new SendSmsRequest()
.setPhoneNumbers(phones)
.setSignName(properties.getSignName())
.setTemplateCode(templateId)
.setTemplateParam(JsonUtils.toJsonString(param));
try {
SendSmsResponse resp = client.sendSms(req);
return SmsResult.builder()
.isSuccess("OK".equals(resp.getBody().getCode()))
.message(resp.getBody().getMessage())
.response(JsonUtils.toJsonString(resp))
.build();
} catch (Exception e) {
throw new SmsException(e.getMessage());
}
}
}

View File

@@ -1,26 +0,0 @@
package org.dromara.common.sms.core;
import org.dromara.common.sms.entity.SmsResult;
import java.util.Map;
/**
* 短信模板
*
* @author Lion Li
* @version 4.2.0
*/
public interface SmsTemplate {
/**
* 发送短信
*
* @param phones 电话号(多个逗号分割)
* @param templateId 模板id
* @param param 模板对应参数
* 阿里 需使用 模板变量名称对应内容 例如: code=1234
* 腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
*/
SmsResult send(String phones, String templateId, Map<String, String> param);
}

View File

@@ -1,82 +0,0 @@
package org.dromara.common.sms.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.sms.config.properties.SmsProperties;
import org.dromara.common.sms.entity.SmsResult;
import org.dromara.common.sms.exception.SmsException;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20190711.SmsClient;
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20190711.models.SendStatus;
import lombok.SneakyThrows;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Tencent 短信模板
*
* @author Lion Li
* @version 4.2.0
*/
public class TencentSmsTemplate implements SmsTemplate {
private SmsProperties properties;
private SmsClient client;
@SneakyThrows(Exception.class)
public TencentSmsTemplate(SmsProperties smsProperties) {
this.properties = smsProperties;
Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint(smsProperties.getEndpoint());
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
this.client = new SmsClient(credential, "", clientProfile);
}
@Override
public SmsResult send(String phones, String templateId, Map<String, String> param) {
if (StringUtils.isBlank(phones)) {
throw new SmsException("手机号不能为空");
}
if (StringUtils.isBlank(templateId)) {
throw new SmsException("模板ID不能为空");
}
SendSmsRequest req = new SendSmsRequest();
Set<String> set = Arrays.stream(phones.split(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
if (CollUtil.isNotEmpty(param)) {
req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
}
req.setTemplateID(templateId);
req.setSign(properties.getSignName());
req.setSmsSdkAppid(properties.getSdkAppId());
try {
SendSmsResponse resp = client.SendSms(req);
SmsResult.SmsResultBuilder builder = SmsResult.builder()
.isSuccess(true)
.message("send success")
.response(JsonUtils.toJsonString(resp));
for (SendStatus sendStatus : resp.getSendStatusSet()) {
if (!"Ok".equals(sendStatus.getCode())) {
builder.isSuccess(false).message(sendStatus.getMessage());
break;
}
}
return builder.build();
} catch (Exception e) {
throw new SmsException(e.getMessage());
}
}
}

View File

@@ -1,31 +0,0 @@
package org.dromara.common.sms.entity;
import lombok.Builder;
import lombok.Data;
/**
* 上传返回体
*
* @author Lion Li
*/
@Data
@Builder
public class SmsResult {
/**
* 是否成功
*/
private boolean isSuccess;
/**
* 响应消息
*/
private String message;
/**
* 实际响应体
* <p>
* 可自行转换为 SDK 对应的 SendSmsResponse
*/
private String response;
}

View File

@@ -1,19 +0,0 @@
package org.dromara.common.sms.exception;
import java.io.Serial;
/**
* Sms异常类
*
* @author Lion Li
*/
public class SmsException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
public SmsException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-social</artifactId>
<description>
ruoyi-common-social 授权认证
</description>
<dependencies>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,23 @@
package org.dromara.common.social.config;
import me.zhyd.oauth.cache.AuthStateCache;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.utils.AuthRedisStateCache;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* Social 配置属性
* @author thiszhc
*/
@AutoConfiguration
@EnableConfigurationProperties(SocialProperties.class)
public class SocialAutoConfiguration {
@Bean
public AuthStateCache authStateCache() {
return new AuthRedisStateCache();
}
}

View File

@@ -0,0 +1,68 @@
package org.dromara.common.social.config.properties;
import lombok.Data;
/**
* 社交登录配置
*
* @author thiszhc
*/
@Data
public class SocialLoginConfigProperties {
/**
* 应用 ID
*/
private String clientId;
/**
* 应用密钥
*/
private String clientSecret;
/**
* 回调地址
*/
private String redirectUri;
/**
* 是否获取unionId
*/
private boolean unionId;
/**
* Coding 企业名称
*/
private String codingGroupName;
/**
* 支付宝公钥
*/
private String alipayPublicKey;
/**
* 企业微信应用ID
*/
private String agentId;
/**
* stackoverflow api key
*/
private String stackOverflowKey;
/**
* 设备ID
*/
private String deviceId;
/**
* 客户端系统类型
*/
private String clientOsType;
/**
* maxkey 服务器地址
*/
private String serverUrl;
}

View File

@@ -0,0 +1,29 @@
package org.dromara.common.social.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Social 配置属性
*
* @author thiszhc
*/
@Data
@Component
@ConfigurationProperties(prefix = "justauth")
public class SocialProperties {
/**
* 是否启用
*/
private Boolean enabled;
/**
* 授权类型
*/
private Map<String, SocialLoginConfigProperties> type;
}

View File

@@ -0,0 +1,80 @@
package org.dromara.common.social.maxkey;
import cn.hutool.core.lang.Dict;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
/**
* @author 长春叭哥 2023年03月26日
*/
public class AuthMaxKeyRequest extends AuthDefaultRequest {
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
/**
* 设定归属域
*/
public AuthMaxKeyRequest(AuthConfig config) {
super(config, AuthMaxKeySource.MAXKEY);
}
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
}
@Override
protected AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
@Override
protected AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthUser.builder()
.uuid(object.getStr("id"))
.username(object.getStr("username"))
.nickname(object.getStr("name"))
.avatar(object.getStr("avatar_url"))
.blog(object.getStr("web_url"))
.company(object.getStr("organization"))
.location(object.getStr("location"))
.email(object.getStr("email"))
.remark(object.getStr("bio"))
.token(authToken)
.source(source.toString())
.build();
}
}

View File

@@ -0,0 +1,52 @@
package org.dromara.common.social.maxkey;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* Oauth2 默认接口说明
*
* @author 长春叭哥 2023年03月26日
*
*/
public enum AuthMaxKeySource implements AuthSource {
/**
* 自己搭建的 maxkey 私服
*/
MAXKEY {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/authorize";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/api/oauth/v20/me";
}
/**
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
*/
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthMaxKeyRequest.class;
}
}
}

View File

@@ -0,0 +1,61 @@
package org.dromara.common.social.utils;
import lombok.AllArgsConstructor;
import me.zhyd.oauth.cache.AuthStateCache;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.redis.utils.RedisUtils;
import java.time.Duration;
/**
* 授权状态缓存
*/
@AllArgsConstructor
public class AuthRedisStateCache implements AuthStateCache {
/**
* 存入缓存
*
* @param key 缓存key
* @param value 缓存内容
*/
@Override
public void cache(String key, String value) {
// 授权超时时间 默认三分钟
RedisUtils.setCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key, value, Duration.ofMinutes(3));
}
/**
* 存入缓存
*
* @param key 缓存key
* @param value 缓存内容
* @param timeout 指定缓存过期时间(毫秒)
*/
@Override
public void cache(String key, String value, long timeout) {
RedisUtils.setCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key, value, Duration.ofMillis(timeout));
}
/**
* 获取缓存内容
*
* @param key 缓存key
* @return 缓存内容
*/
@Override
public String get(String key) {
return RedisUtils.getCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key);
}
/**
* 是否存在key如果对应key的value值已过期也返回false
*
* @param key 缓存key
* @return true存在key并且value没过期falsekey不存在或者已过期
*/
@Override
public boolean containsKey(String key) {
return RedisUtils.hasKey(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key);
}
}

View File

@@ -0,0 +1,70 @@
package org.dromara.common.social.utils;
import cn.hutool.core.util.ObjectUtil;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.*;
import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
/**
* 认证授权工具类
*
* @author thiszhc
*/
public class SocialUtils {
private static final AuthRedisStateCache STATE_CACHE = SpringUtils.getBean(AuthRedisStateCache.class);
@SuppressWarnings("unchecked")
public static AuthResponse<AuthUser> loginAuth(LoginBody loginBody, SocialProperties socialProperties) throws AuthException {
AuthRequest authRequest = getAuthRequest(loginBody.getSource(), socialProperties);
AuthCallback callback = new AuthCallback();
callback.setCode(loginBody.getSocialCode());
callback.setState(loginBody.getSocialState());
return authRequest.login(callback);
}
public static AuthRequest getAuthRequest(String source, SocialProperties socialProperties) throws AuthException {
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
if (ObjectUtil.isNull(obj)) {
throw new AuthException("不支持的第三方登录类型");
}
String clientId = obj.getClientId();
String clientSecret = obj.getClientSecret();
String redirectUri = obj.getRedirectUri();
return switch (source.toLowerCase()) {
case "dingtalk" -> new AuthDingTalkRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "baidu" -> new AuthBaiduRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "github" -> new AuthGithubRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "gitee" -> new AuthGiteeRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "weibo" -> new AuthWeiboRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "coding" -> new AuthCodingRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "oschina" -> new AuthOschinaRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
// 支付宝在创建回调地址时不允许使用localhost或者127.0.0.1所以这儿的回调地址使用的局域网内的ip
case "alipay" -> new AuthAlipayRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), socialProperties.getType().get("alipay").getAlipayPublicKey(), STATE_CACHE);
case "qq" -> new AuthQqRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "wechat_open" -> new AuthWeChatOpenRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "taobao" -> new AuthTaobaoRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "douyin" -> new AuthDouyinRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "linkedin" -> new AuthLinkedinRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "microsoft" -> new AuthMicrosoftRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "renren" -> new AuthRenrenRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "stack_overflow" -> new AuthStackOverflowRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).stackOverflowKey("").build(), STATE_CACHE);
case "huawei" -> new AuthHuaweiRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).agentId("").build(), STATE_CACHE);
case "gitlab" -> new AuthGitlabRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "wechat_mp" -> new AuthWeChatMpRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "aliyun" -> new AuthAliyunRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
case "maxkey" -> new AuthMaxKeyRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
default -> throw new AuthException("未获取到有效的Auth配置");
};
}
}

View File

@@ -0,0 +1 @@
org.dromara.common.social.config.SocialAutoConfiguration