init: 导入RuoYi‑Vue‑Plus 6.X完整代码

This commit is contained in:
admin
2026-08-09 17:51:11 +08:00
commit 51cac459b3
914 changed files with 103833 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi-vue-plus</artifactId>
<groupId>org.dromara</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common</artifactId>
<packaging>pom</packaging>
<description>
common 通用模块
</description>
<modules>
<module>ruoyi-common-bom</module>
<module>ruoyi-common-social</module>
<module>ruoyi-common-captcha</module>
<module>ruoyi-common-core</module>
<module>ruoyi-common-doc</module>
<module>ruoyi-common-excel</module>
<module>ruoyi-common-job</module>
<module>ruoyi-common-log</module>
<module>ruoyi-common-mail</module>
<module>ruoyi-common-mybatis</module>
<module>ruoyi-common-oss</module>
<module>ruoyi-common-redis</module>
<module>ruoyi-common-satoken</module>
<module>ruoyi-common-security</module>
<module>ruoyi-common-sms</module>
<module>ruoyi-common-elasticsearch</module>
<module>ruoyi-common-web</module>
<module>ruoyi-common-translation</module>
<module>ruoyi-common-sensitive</module>
<module>ruoyi-common-json</module>
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-push</module>
<module>ruoyi-common-liteflow</module>
<module>ruoyi-common-mqtt</module>
<module>ruoyi-common-ai</module>
<module>ruoyi-common-mcp</module>
</modules>
</project>
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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-ai</artifactId>
<description>
ruoyi-common-ai AI公共模块
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- Snail AI Agent 聊天室 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-agent-chat-starter</artifactId>
</dependency>
<!-- Snail AI Agent 执行器 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-agent-executor-starter</artifactId>
</dependency>
<!-- Snail AI OpenAPI 启动器 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-openapi-starter</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,16 @@
package org.dromara.common.ai.config;
import com.aizuda.snail.ai.agent.starter.EnableSnailAiAgent;
import com.aizuda.snail.ai.openapi.client.starter.EnableSnailAiOpenApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* Snail AI 自动配置
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "snail-ai", name = "enabled", havingValue = "true")
@EnableSnailAiAgent
@EnableSnailAiOpenApi
public class SnailAiConfig {
}
+200
View File
@@ -0,0 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-bom</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<description>
ruoyi-common-bom common依赖项
</description>
<properties>
<revision>6.0.0</revision>
</properties>
<dependencyManagement>
<dependencies>
<!-- 验证码模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-captcha</artifactId>
<version>${revision}</version>
</dependency>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 接口模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
<version>${revision}</version>
</dependency>
<!-- excel -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
<version>${revision}</version>
</dependency>
<!-- 调度模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-job</artifactId>
<version>${revision}</version>
</dependency>
<!-- 日志记录 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
<version>${revision}</version>
</dependency>
<!-- 邮件服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据库服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
<version>${revision}</version>
</dependency>
<!-- OSS -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-oss</artifactId>
<version>${revision}</version>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
<version>${revision}</version>
</dependency>
<!-- satoken -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
<version>${revision}</version>
</dependency>
<!-- 短信模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sms</artifactId>
<version>${revision}</version>
</dependency>
<!-- ES搜索引擎服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-elasticsearch</artifactId>
<version>${revision}</version>
</dependency>
<!-- 授权认证 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-social</artifactId>
<version>${revision}</version>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
<version>${revision}</version>
</dependency>
<!-- 翻译模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-translation</artifactId>
<version>${revision}</version>
</dependency>
<!-- 脱敏模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sensitive</artifactId>
<version>${revision}</version>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据库加解密模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-encrypt</artifactId>
<version>${revision}</version>
</dependency>
<!-- 消息推送模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-push</artifactId>
<version>${revision}</version>
</dependency>
<!-- liteflow模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-liteflow</artifactId>
<version>${revision}</version>
</dependency>
<!-- mqtt模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mqtt</artifactId>
<version>${revision}</version>
</dependency>
<!-- ai模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ai</artifactId>
<version>${revision}</version>
</dependency>
<!-- mcp模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mcp</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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-captcha</artifactId>
<description>
ruoyi-common-code 验证码模块
</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-api</artifactId>
<version>3.3.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-core</artifactId>
<version>3.3.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,45 @@
package org.dromara.common.captcha.config;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import org.dromara.common.captcha.config.properties.CaptchaProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 验证码配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableConfigurationProperties(CaptchaProperties.class)
@Configuration
public class CaptchaConfig {
@Bean
public ResourceStore resourceStore() {
// 使用简单的本地内存存储器,实际项目中可以使用数据库等存储
LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();
// 2. 添加自定义背景图片
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg", "default"));
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg", "default"));
// resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/c.jpg", "default"));
return resourceStore;
}
}
@@ -0,0 +1,34 @@
package org.dromara.common.captcha.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 验证码 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "image-captcha")
public class CaptchaProperties {
/**
* 是否启用验证码校验。
*/
private Boolean enable;
/**
* 验证码类型
*/
private String type;
/**
* 数字验证码位数
*/
private Integer numberLength;
/**
* 字符验证码长度
*/
private Integer charLength;
}
@@ -0,0 +1,258 @@
package org.dromara.common.captcha.utils;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.captcha.config.properties.CaptchaProperties;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.regex.RegexValidator;
import org.dromara.common.mail.core.MailBuilder;
import org.dromara.common.redis.annotation.RateLimiter;
import org.dromara.common.redis.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.web.core.WaveAndCircleCaptcha;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import java.awt.*;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.concurrent.ThreadLocalRandom;
/**
* 验证码工具类
* 支持:图形验证码、手机短信验证码、邮箱验证码、行为验证码
* 规范:一次下发生成验证码、二次校验验证码;内置通用随机码生成、图片生成基础能力
*
* @author xiaozhi
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CaptchaUtils {
private final CaptchaProperties captchaProperties;
private final ImageCaptchaApplication imageCaptchaApplication;
/**
* SpEL解析器,单例复用,避免频繁创建
*/
private final ExpressionParser expressionParser = new SpelExpressionParser();
// ====================== 【图形验证码】 ======================
/**
* 图片验证码响应对象。
*
* @param captchaEnabled 是否启用验证码
* @param uuid 验证码标识
* @param img Base64 图片数据
*/
public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) {
}
/**
* 生成图形验证码
* 限流:同一IP 60秒最多10次请求
*
* @return CaptchaVo
*/
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
public CaptchaVo generateImageCaptcha() {
boolean captchaEnabled = captchaProperties.getEnable();
if (!captchaEnabled) {
return new CaptchaVo(false, null, null);
}
// 保存验证码信息
String uuid = IdUtil.simpleUUID();
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
String captchaType = captchaProperties.getType();
CodeGenerator codeGenerator;
if ("math".equals(captchaType)) {
codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false);
} else {
codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
}
WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
// captcha.setBackground(Color.WHITE); // 不设置就是透明底
captcha.setFont(new Font("Arial", Font.BOLD, 45));
captcha.setGenerator(codeGenerator);
captcha.createCode();
String code = captcha.getCode();
// 数学验证码,计算表达式结果存储
if ("math".equals(captchaType)) {
Expression exp = expressionParser.parseExpression(StringUtils.remove(code, "="));
code = exp.getValue(String.class);
}
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
return new CaptchaVo(true, uuid, captcha.getImageBase64());
}
/**
* 校验图形验证码
* <p>【一次性消费】校验通过自动删除缓存,防止重复使用、重放攻击
*
* @param uuid 前端传递验证码唯一标识
* @param inputCode 用户输入验证码
* @return true=验证通过 false=验证失败
*/
public boolean verifyImageCaptcha(String uuid, String inputCode) {
return true;
}
// ====================== 【手机短信验证码】 ======================
/**
* 生成手机短信验证码并发送
*
* @param phoneNumber 手机号
*/
public void generateSmsCaptcha(String phoneNumber) {
if (!RegexValidator.isMobile(phoneNumber)) {
return;
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
String code = RandomUtil.randomNumbers(6);
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse;
try {
smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
} catch (Exception e) {
log.error("验证码短信发送异常,手机号:{}", phoneNumber, e);
return;
}
if (!smsResponse.isSuccess()) {
log.error("验证码短信发送失败 => {}", smsResponse);
return;
}
// 发送成功才写入缓存
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
}
/**
* 校验短信验证码
* <p>【一次性消费】校验成功自动删除缓存
*
* @param phoneNumber 手机号
* @param inputCode 用户输入验证码
* @return 是否通过
*/
public boolean verifySmsCaptcha(String phoneNumber, String inputCode) {
return true;
}
// ====================== 【邮箱验证码】 ======================
/**
* 生成邮箱验证码并发送
* 限流:同一个邮箱60秒最多1次
*
* @param email 邮箱地址
*/
@RateLimiter(key = "#email", time = 60, count = 1)
public void generateEmailCaptcha(String email) {
if (!RegexValidator.isEmail(email)) {
return;
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(6);
try {
MailBuilder.of().to(email).subject("登录验证码").text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。").send();
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
}
}
/**
* 校验邮箱验证码
*
* @param inputCode 用户输入
* @param cacheCode 缓存验证码
* @return 是否通过
*/
public boolean verifyEmailCaptcha(String inputCode, String cacheCode) {
return true;
}
// ====================== 【行为验证码(滑块/点选)】 ======================
/**
* 生成行为验证码初始票据 ticket
* 用于前端拉取滑块背景图、滑块图
*
* @return ticket字符串
*/
public ApiResponse<ImageCaptchaVO> generateBehaviorCaptchaTicket() {
String type;
int i = ThreadLocalRandom.current().nextInt(0, 4);
if (i == 0) {
type = CaptchaTypeConstant.SLIDER;
} else if (i == 1) {
type = CaptchaTypeConstant.CONCAT;
} else if (i == 2) {
type = CaptchaTypeConstant.ROTATE;
} else {
type = CaptchaTypeConstant.WORD_IMAGE_CLICK;
}
GenerateParam generateParam = new GenerateParam();
// 要生成的验证码类型
generateParam.setType(type);
return imageCaptchaApplication.generateCaptcha(generateParam);
}
/**
* 行为验证码一次校验(滑块坐标、轨迹校验由第三方SDK/自研逻辑实现)
* 本方法仅做凭证匹配占位,实际行为判定需要额外业务逻辑
*
* @param ticket 请求票据
* @param cacheTicket 缓存票据
* @return 票据是否匹配
*/
public ApiResponse<?> verifyBehaviorCaptchaFirstStage(String id, ImageCaptchaTrack data) {
ApiResponse<?> response = imageCaptchaApplication.matching(id, data);
if (response.isSuccess()) {
return ApiResponse.ofSuccess(Collections.singletonMap("id", id));
}
return response;
}
/**
* 行为验证码二次验证(业务接口调用,例如登录接口)
*
* @param verifyTicket 前端传入二次凭证
* @param cacheVerifyTicket 服务端缓存凭证
* @return 验证结果
*/
public boolean verifyBehaviorCaptchaSecondStage(String verifyTicket, String cacheVerifyTicket) {
return true;
}
}
@@ -0,0 +1 @@
org.dromara.common.captcha.config.CaptchaConfig
+106
View File
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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-core</artifactId>
<description>
ruoyi-common-core 核心模块
</description>
<dependencies>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- SpringWeb模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- 自定义验证注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- AOP 切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<!-- Hutool 核心工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<!-- Hutool HTTP 工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
<!-- Hutool 扩展工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 自动生成YML配置关联JSON文件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- Spring Boot 配置迁移 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Mapstruct Plus -->
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
</dependency>
<!-- 离线IP地址定位库 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,17 @@
package org.dromara.common.core.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* 程序注解配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableAspectJAutoProxy
@EnableAsync(proxyTargetClass = true)
public class ApplicationConfig {
}
@@ -0,0 +1,119 @@
package org.dromara.common.core.config;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import java.util.concurrent.*;
/**
* 线程池配置
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
public class ThreadPoolConfig {
/**
* 核心线程数 = cpu 核心数 + 1
*/
private final int core = Runtime.getRuntime().availableProcessors() + 1;
private ScheduledExecutorService scheduledExecutorService;
/**
* 执行周期性或定时任务
*
* @return 全局定时任务线程池
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
// daemon 必须为 true
BasicThreadFactory.Builder builder = BasicThreadFactory.builder().daemon(true);
if (SpringUtils.isVirtual()) {
builder.namingPattern("virtual-schedule-pool-%d").wrappedFactory(new VirtualThreadTaskExecutor().getVirtualThreadFactory());
} else {
builder.namingPattern("schedule-pool-%d");
}
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(core,
builder.build(),
new ThreadPoolExecutor.CallerRunsPolicy()) {
/**
* 定时任务执行完成后统一打印异常。
*
* @param r 已执行的任务
* @param t 任务执行异常
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
printException(r, t);
}
};
this.scheduledExecutorService = scheduledThreadPoolExecutor;
return scheduledThreadPoolExecutor;
}
/**
* 销毁事件
* 停止线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
* 如果仍然超時,則強制退出.
* 另对在shutdown时线程本身被调用中断做了处理.
*/
@PreDestroy
public void destroy() {
try {
log.info("====关闭后台任务任务线程池====");
ScheduledExecutorService pool = scheduledExecutorService;
if (pool != null && !pool.isShutdown()) {
pool.shutdown();
try {
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
log.info("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
/**
* 打印线程异常信息
*
* @param r 已执行的任务
* @param t 任务执行过程中抛出的异常
*/
public static void printException(Runnable r, Throwable t) {
if (t == null && r instanceof Future<?> future) {
try {
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (t != null) {
log.error(t.getMessage(), t);
}
}
}
@@ -0,0 +1,41 @@
package org.dromara.common.core.config;
import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import java.util.Properties;
/**
* 校验框架配置类
*
* @author Lion Li
*/
@AutoConfiguration(before = ValidationAutoConfiguration.class)
public class ValidatorConfig {
/**
* 配置校验框架 快速失败模式
*
* @param messageSource 国际化消息源
* @return 启用快速失败模式的校验器
*/
@Bean
public Validator validator(MessageSource messageSource) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
// 国际化
factoryBean.setValidationMessageSource(messageSource);
// 设置使用 HibernateValidator 校验器
factoryBean.setProviderClass(HibernateValidator.class);
Properties properties = new Properties();
// 设置快速失败模式(fail-fast),即校验过程中一旦遇到失败,立即停止并返回错误
properties.setProperty("hibernate.validator.fail_fast", "true");
factoryBean.setValidationProperties(properties);
return factoryBean;
}
}
@@ -0,0 +1,89 @@
package org.dromara.common.core.constant;
/**
* 缓存组名称常量,统一约定缓存名和缓存策略配置格式。
* <p>
* key 格式为 cacheNames#ttl#maxIdleTime#maxSize#local
* <p>
* ttl 过期时间 如果设置为0则不过期 默认为0
* maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
* maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
* local 默认开启本地缓存为1 关闭本地缓存为0
* <p>
* 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500、test#1h#0#500#0
*
* @author Lion Li
*/
public interface CacheNames {
/**
* 演示案例
*/
String DEMO_CACHE = "demo:cache#60s#10m#20";
/**
* 系统配置
*/
String SYS_CONFIG = "sys_config";
/**
* 数据字典
*/
String SYS_DICT = "sys_dict";
/**
* 数据字典类型
*/
String SYS_DICT_TYPE = "sys_dict_type";
/**
* 客户端
*/
String SYS_CLIENT = "sys_client#30d";
/**
* 用户账户
*/
String SYS_USER_NAME = "sys_user_name#30d";
/**
* 用户昵称
*/
String SYS_NICKNAME = "sys_nickname#30d";
/**
* 部门
*/
String SYS_DEPT = "sys_dept#30d";
/**
* OSS内容
*/
String SYS_OSS = "sys_oss#30d";
/**
* 角色自定义权限
*/
String SYS_ROLE_CUSTOM = "sys_role_custom#30d";
/**
* 部门及以下权限
*/
String SYS_DEPT_AND_CHILD = "sys_dept_and_child#30d";
/**
* OSS配置
*/
String SYS_OSS_CONFIG = "sys_oss_config";
/**
* 在线用户
*/
String ONLINE_TOKEN_KEY = "online_tokens:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
}
@@ -0,0 +1,81 @@
package org.dromara.common.core.constant;
/**
* 通用基础常量定义。
*
* @author ruoyi
*/
public interface Constants {
/**
* UTF-8 字符集
*/
String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
String GBK = "GBK";
/**
* www主域
*/
String WWW = "www.";
/**
* http请求
*/
String HTTP = "http://";
/**
* https请求
*/
String HTTPS = "https://";
/**
* 通用成功标识
*/
String SUCCESS = "0";
/**
* 通用失败标识
*/
String FAIL = "1";
/**
* 登录成功
*/
String LOGIN_SUCCESS = "Success";
/**
* 注销
*/
String LOGOUT = "Logout";
/**
* 注册
*/
String REGISTER = "Register";
/**
* 登录失败
*/
String LOGIN_FAIL = "Error";
/**
* 验证码有效期(分钟)
*/
Integer CAPTCHA_EXPIRATION = 2;
/**
* 顶级父级id
*/
Long TOP_PARENT_ID = 0L;
/**
* 加密头
*/
String ENCRYPT_HEADER = "ENC_";
}
@@ -0,0 +1,34 @@
package org.dromara.common.core.constant;
/**
* 全局通用键常量,主要用于业务无关的 Redis Key 前缀定义。
*
* @author Lion Li
*/
public interface GlobalConstants {
/**
* 全局 redis key (业务无关的key)
*/
String GLOBAL_REDIS_KEY = "global:";
/**
* 验证码 redis key
*/
String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
/**
* 防重提交 redis key
*/
String REPEAT_SUBMIT_KEY = GLOBAL_REDIS_KEY + "repeat_submit:";
/**
* 限流 redis key
*/
String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
/**
* 三方认证 redis key
*/
String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
}
@@ -0,0 +1,93 @@
package org.dromara.common.core.constant;
/**
* 返回状态码
*
* @author Lion Li
*/
public interface HttpStatus {
/**
* 操作成功
*/
int SUCCESS = 200;
/**
* 对象创建成功
*/
int CREATED = 201;
/**
* 请求已经被接受
*/
int ACCEPTED = 202;
/**
* 操作已经执行成功,但是没有返回数据
*/
int NO_CONTENT = 204;
/**
* 资源已被移除
*/
int MOVED_PERM = 301;
/**
* 重定向
*/
int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
int NOT_MODIFIED = 304;
/**
* 参数列表错误(缺少,格式不匹配)
*/
int BAD_REQUEST = 400;
/**
* 未授权
*/
int UNAUTHORIZED = 401;
/**
* 访问受限,授权过期
*/
int FORBIDDEN = 403;
/**
* 资源,服务未找到
*/
int NOT_FOUND = 404;
/**
* 不允许的http方法
*/
int BAD_METHOD = 405;
/**
* 资源冲突,或者资源被锁
*/
int CONFLICT = 409;
/**
* 不支持的数据,媒体类型
*/
int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
int ERROR = 500;
/**
* 接口未实现
*/
int NOT_IMPLEMENTED = 501;
/**
* 系统警告消息
*/
int WARN = 601;
}
@@ -0,0 +1,59 @@
package org.dromara.common.core.constant;
import cn.hutool.core.lang.RegexPool;
/**
* 常用正则表达式字符串
* <p>
* 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/
*
* @author AprilWind
*/
public interface RegexConstants extends RegexPool {
/**
* 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
*/
String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";
/**
* 权限标识必须符合以下格式:
* 1. 标准格式:xxx:yyy:zzz
* - 第一部分(xxx):只能包含字母、数字和下划线(_),不能使用 `*`
* - 第二部分(yyy):可以包含字母、数字、下划线(_)和 `*`
* - 第三部分(zzz):可以包含字母、数字、下划线(_)和 `*`
* 2. 允许空字符串(""),表示没有权限标识
*/
String PERMISSION_STRING = "^$|^[a-zA-Z0-9_]+:[a-zA-Z0-9_*]+:[a-zA-Z0-9_*]+$";
/**
* 身份证号码(后6位)
*/
String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
/**
* QQ号码
*/
String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$";
/**
* 邮政编码
*/
String POSTAL_CODE = "^[1-9]\\d{5}$";
/**
* 注册账号
*/
String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$";
/**
* 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
*/
String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
/**
* 通用状态(0表示正常,1表示停用)
*/
String STATUS = "^[01]$";
}
@@ -0,0 +1,91 @@
package org.dromara.common.core.constant;
/**
* 系统常量信息
*
* @author Lion Li
*/
public interface SystemConstants {
/**
* 正常状态
*/
String NORMAL = "0";
/**
* 异常状态
*/
String DISABLE = "1";
/**
* 是
*/
String YES = "Y";
/**
* 否
*/
String NO = "N";
/**
* 菜单类型(目录)
*/
String TYPE_DIR = "M";
/**
* 菜单类型(菜单)
*/
String TYPE_MENU = "C";
/**
* 菜单类型(按钮)
*/
String TYPE_BUTTON = "F";
/**
* Layout组件标识
*/
String LAYOUT = "Layout";
/**
* ParentView组件标识
*/
String PARENT_VIEW = "ParentView";
/**
* InnerLink组件标识
*/
String INNER_LINK = "InnerLink";
/**
* 超级管理员用户ID
*/
Long SUPER_ADMIN_USER_ID = 1761100000000000001L;
/**
* 超级管理员角色ID
*/
Long SUPER_ADMIN_ROLE_ID = 1761300000000000001L;
/**
* 超级管理员角色 roleKey
*/
String SUPER_ADMIN_ROLE_KEY = "superadmin";
/**
* 根部门祖级列表
*/
String ROOT_DEPT_ANCESTORS = "0";
/**
* 默认部门 ID
*/
Long DEFAULT_DEPT_ID = 1761000000000000100L;
/**
* 排除敏感属性字段
*/
String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};
}
@@ -0,0 +1,95 @@
package org.dromara.common.core.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
/**
* 表格分页数据对象
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class PageResult<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private long total;
/**
* 列表数据
*/
private Collection<T> rows;
/**
* 分页
*
* @param list 列表数据
* @param total 总记录数
*/
public PageResult(Collection<T> list, long total) {
this.rows = emptyIfNull(list);
this.total = total;
}
/**
* 根据列表和总数构建表格分页数据对象。
*
* @param list 列表数据
* @param total 总记录数
* @param <T> 列表数据类型
* @return 表格分页数据对象
*/
public static <T> PageResult<T> build(Collection<T> list, long total) {
PageResult<T> rspData = new PageResult<>();
rspData.setRows(emptyIfNull(list));
rspData.setTotal(total);
return rspData;
}
/**
* 根据数据列表构建表格分页数据对象。
*
* @param list 列表数据
* @param <T> 列表数据类型
* @return 表格分页数据对象
*/
public static <T> PageResult<T> build(Collection<T> list) {
PageResult<T> rspData = new PageResult<>();
Collection<T> rows = emptyIfNull(list);
rspData.setRows(rows);
rspData.setTotal(rows.size());
return rspData;
}
/**
* 构建空表格分页数据对象。
*
* @param <T> 列表数据类型
* @return 表格分页数据对象
*/
public static <T> PageResult<T> build() {
return new PageResult<>();
}
/**
* 空集合兜底处理。
*
* @param list 原始集合
* @param <T> 集合元素类型
* @return 非 null 集合
*/
private static <T> Collection<T> emptyIfNull(Collection<T> list) {
return list == null ? Collections.emptyList() : list;
}
}
@@ -0,0 +1,214 @@
package org.dromara.common.core.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dromara.common.core.constant.HttpStatus;
import java.io.Serial;
import java.io.Serializable;
import static org.dromara.common.core.constant.HttpStatus.ERROR;
import static org.dromara.common.core.constant.HttpStatus.SUCCESS;
/**
* 响应信息主体
*
* @param <T> 响应数据的泛型类型
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class R<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 响应状态码
*/
private int code;
/**
* 响应提示信息
*/
private String msg;
/**
* 响应业务数据
*/
private T data;
/**
* 构建成功响应结果
*
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok() {
return restResult(null, SUCCESS, "操作成功");
}
/**
* 构建成功响应结果(带业务数据)
*
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok(T data) {
return restResult(data, SUCCESS, "操作成功");
}
/**
* 构建成功响应结果(明确指定业务数据)
*
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> data(T data) {
return restResult(data, SUCCESS, "操作成功");
}
/**
* 构建成功响应结果(自定义提示信息)
*
* @param msg 自定义提示信息
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok(String msg) {
return restResult(null, SUCCESS, msg);
}
/**
* 构建成功响应结果(自定义提示信息+业务数据)
*
* @param msg 自定义提示信息
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok(String msg, T data) {
return restResult(data, SUCCESS, msg);
}
/**
* 构建失败响应结果
*
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail() {
return restResult(null, ERROR, "操作失败");
}
/**
* 构建失败响应结果(自定义提示信息)
*
* @param msg 自定义提示信息
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(String msg) {
return restResult(null, ERROR, msg);
}
/**
* 构建失败响应结果(带业务数据)
*
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(T data) {
return restResult(data, ERROR, "操作失败");
}
/**
* 构建失败响应结果(自定义提示信息+业务数据)
*
* @param msg 自定义提示信息
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(String msg, T data) {
return restResult(data, ERROR, msg);
}
/**
* 构建失败响应结果(自定义状态码+提示信息)
*
* @param code 自定义状态码
* @param msg 自定义提示信息
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(int code, String msg) {
return restResult(null, code, msg);
}
/**
* 构建警告响应结果
*
* @param msg 自定义提示信息
* @param <T> 响应数据的泛型类型
* @return 警告响应结果对象
*/
public static <T> R<T> warn(String msg) {
return restResult(null, HttpStatus.WARN, msg);
}
/**
* 构建警告响应结果(自定义提示信息+业务数据)
*
* @param msg 自定义提示信息
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 警告响应结果对象
*/
public static <T> R<T> warn(String msg, T data) {
return restResult(data, HttpStatus.WARN, msg);
}
/**
* 核心构建方法
*
* @param data 业务数据
* @param code 响应状态码
* @param msg 提示信息
* @param <T> 响应数据的泛型类型
* @return 响应结果对象
*/
private static <T> R<T> restResult(T data, int code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
/**
* 判断响应结果是否为失败
*
* @param ret 响应结果对象
* @param <T> 响应数据的泛型类型
* @return true=失败,false=成功
*/
public static <T> Boolean isError(R<T> ret) {
return !isSuccess(ret);
}
/**
* 判断响应结果是否为成功
*
* @param ret 响应结果对象
* @param <T> 响应数据的泛型类型
* @return true=成功,false=失败
*/
public static <T> Boolean isSuccess(R<T> ret) {
return ret != null && SUCCESS == ret.getCode();
}
}
@@ -0,0 +1,41 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 字典数据DTO
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class DictDataDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 字典标签
*/
private String dictLabel;
/**
* 字典键值
*/
private String dictValue;
/**
* 是否默认(Y是 N否)
*/
private String isDefault;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,41 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 字典类型DTO
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class DictTypeDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 字典主键
*/
private Long dictId;
/**
* 字典名称
*/
private String dictName;
/**
* 字典类型
*/
private String dictType;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,42 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 通用登录请求对象,封装客户端、授权类型和验证码信息。
*
* @author Lion Li
*/
@Data
public class LoginBody implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端id
*/
@NotBlank(message = "{auth.clientid.not.blank}")
private String clientId;
/**
* 授权类型
*/
@NotBlank(message = "{auth.grant.type.not.blank}")
private String grantType;
/**
* 验证码
*/
private String code;
/**
* 验证码唯一标识。
*/
private String uuid;
}
@@ -0,0 +1,218 @@
package org.dromara.common.core.enums;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 流程业务状态枚举,统一定义单据在审批流转中的状态。
*
* @author may
*/
@Getter
@AllArgsConstructor
public enum BusinessStatusEnum {
/**
* 已撤销
*/
CANCEL("cancel", "已撤销"),
/**
* 草稿
*/
DRAFT("draft", "草稿"),
/**
* 待审核
*/
WAITING("waiting", "待审核"),
/**
* 已完成
*/
FINISH("finish", "已完成"),
/**
* 已作废
*/
INVALID("invalid", "已作废"),
/**
* 已退回
*/
BACK("back", "已退回"),
/**
* 已终止
*/
TERMINATION("termination", "已终止");
/**
* 状态
*/
private final String status;
/**
* 描述
*/
private final String desc;
/**
* 业务状态枚举缓存。
*/
private static final Map<String, BusinessStatusEnum> STATUS_MAP = Arrays.stream(BusinessStatusEnum.values())
.collect(Collectors.toConcurrentMap(BusinessStatusEnum::getStatus, Function.identity()));
/**
* 根据状态获取对应的 BusinessStatusEnum 枚举
*
* @param status 业务状态码
* @return 对应的 BusinessStatusEnum 枚举,如果找不到则返回 null
*/
public static BusinessStatusEnum getByStatus(String status) {
// 使用 STATUS_MAP 获取对应的枚举,若找不到则返回 null
return STATUS_MAP.get(status);
}
/**
* 根据状态获取对应的业务状态描述信息
*
* @param status 业务状态码
* @return 返回业务状态描述,若状态码为空或未找到对应的枚举,返回空字符串
*/
public static String findByStatus(String status) {
if (StringUtils.isBlank(status)) {
return StrUtil.EMPTY;
}
BusinessStatusEnum statusEnum = STATUS_MAP.get(status);
return (statusEnum != null) ? statusEnum.getDesc() : StrUtil.EMPTY;
}
/**
* 判断是否为指定的状态之一:草稿、已撤销或已退回
*
* @param status 要检查的状态
* @return 如果状态为草稿、已撤销或已退回之一,则返回 true;否则返回 false
*/
public static boolean isDraftOrCancelOrBack(String status) {
return DRAFT.status.equals(status) || CANCEL.status.equals(status) || BACK.status.equals(status);
}
/**
* 判断是否为撤销,退回,作废,终止
*
* @param status status
* @return 结果
*/
public static boolean initialState(String status) {
return CANCEL.status.equals(status) || BACK.status.equals(status) || INVALID.status.equals(status) || TERMINATION.status.equals(status);
}
/**
* 获取运行中的实例状态列表
*
* @return 包含运行中实例状态的不可变列表
* (包含 DRAFT、WAITING、BACK 和 CANCEL 状态)
*/
public static List<String> runningStatus() {
return Arrays.asList(DRAFT.status, WAITING.status, BACK.status, CANCEL.status);
}
/**
* 获取结束实例的状态列表
*
* @return 包含结束实例状态的不可变列表
* (包含 FINISH、INVALID 和 TERMINATION 状态)
*/
public static List<String> finishStatus() {
return Arrays.asList(FINISH.status, INVALID.status, TERMINATION.status);
}
/**
* 启动流程校验
*
* @param status 状态
*/
public static void checkStartStatus(String status) {
if (WAITING.getStatus().equals(status)) {
throw new ServiceException("该单据已提交过申请,正在审批中!");
} else if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
/**
* 撤销流程校验
*
* @param status 状态
*/
public static void checkCancelStatus(String status) {
if (CANCEL.getStatus().equals(status)) {
throw new ServiceException("该单据已撤销!");
} else if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (BACK.getStatus().equals(status)) {
throw new ServiceException("该单据已退回!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
/**
* 驳回流程校验
*
* @param status 状态
*/
public static void checkBackStatus(String status) {
if (BACK.getStatus().equals(status)) {
throw new ServiceException("该单据已退回!");
} else if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (CANCEL.getStatus().equals(status)) {
throw new ServiceException("该单据已撤销!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
/**
* 作废,终止流程校验
*
* @param status 状态
*/
public static void checkInvalidStatus(String status) {
if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
}
@@ -0,0 +1,39 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 登录设备类型枚举。
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum DeviceType {
/**
* pc端
*/
PC("pc"),
/**
* app端
*/
APP("app"),
/**
* 小程序端
*/
XCX("xcx"),
/**
* 第三方社交登录平台
*/
SOCIAL("social");
/**
* 设备标识
*/
private final String device;
}
@@ -0,0 +1,44 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 登录类型枚举,同时维护不同登录方式对应的重试提示配置。
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum LoginType {
/**
* 密码登录
*/
PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
/**
* 短信登录
*/
SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
/**
* 邮箱登录
*/
EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
/**
* 小程序登录
*/
XCX("", "");
/**
* 登录重试超出限制提示
*/
final String retryLimitExceed;
/**
* 登录重试限制计数提示
*/
final String retryLimitCount;
}
@@ -0,0 +1,44 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 推送消息来源枚举
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum PushSourceEnum {
/**
* 后端系统消息
*/
BACKEND("backend"),
/**
* 通知公告
*/
NOTICE("notice"),
/**
* 工作流
*/
WORKFLOW("workflow"),
/**
* 大模型
*/
LLM("llm"),
/**
* 客户端消息
*/
CLIENT("client");
/**
* 消息来源标识。
*/
private final String source;
}
@@ -0,0 +1,39 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 推送消息类型枚举
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum PushTypeEnum {
/**
* 通用消息
*/
MESSAGE("message"),
/**
* 通知公告
*/
NOTICE("notice"),
/**
* 大模型消息
*/
LLM("llm"),
/**
* 自定义消息
*/
CUSTOM("custom");
/**
* 消息类型标识。
*/
private final String type;
}
@@ -0,0 +1,37 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户状态
*
* @author ruoyi
*/
@Getter
@AllArgsConstructor
public enum UserStatus {
/**
* 正常
*/
OK("0", "正常"),
/**
* 停用
*/
DISABLE("1", "停用"),
/**
* 删除
*/
DELETED("2", "删除");
/**
* 状态编码。
*/
private final String code;
/**
* 状态说明。
*/
private final String info;
}
@@ -0,0 +1,45 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.utils.StringUtils;
/**
* 用户类型
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum UserType {
/**
* 后台系统用户
*/
SYS_USER("sys_user"),
/**
* 移动客户端用户
*/
APP_USER("app_user");
/**
* 用户类型标识(用于 token、权限识别等)
*/
private final String userType;
/**
* 根据字符串内容匹配用户类型。
*
* @param str 待匹配字符串
* @return 用户类型
*/
public static UserType getUserType(String str) {
for (UserType value : values()) {
if (StringUtils.contains(str, value.getUserType())) {
return value;
}
}
throw new RuntimeException("'UserType' not found By " + str);
}
}
@@ -0,0 +1,112 @@
package org.dromara.common.core.exception;
import cn.hutool.core.text.StrFormatter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
* 通用业务异常,支持使用占位符拼接错误信息。
*
* @author ruoyi
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public final class ServiceException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 错误明细,内部调试错误
*/
private String detailMessage;
/**
* 使用错误消息构造业务异常。
*
* @param message 错误消息
*/
public ServiceException(String message) {
this.message = message;
}
/**
* 使用错误消息和错误码构造业务异常。
*
* @param message 错误消息
* @param code 错误码
*/
public ServiceException(String message, Integer code) {
this.message = message;
this.code = code;
}
/**
* 使用错误消息和根因构造业务异常。
*
* @param message 错误消息
* @param cause 根因
*/
public ServiceException(String message, Throwable cause) {
super(cause);
this.message = message;
}
/**
* 使用占位符参数格式化错误消息。
*
* @param message 模板消息
* @param args 参数
*/
public ServiceException(String message, Object... args) {
this.message = StrFormatter.format(message, args);
}
/**
* 获取错误提示。
*
* @return 错误提示
*/
@Override
public String getMessage() {
return message;
}
/**
* 设置错误消息并返回当前异常对象。
*
* @param message 错误消息
* @return 当前异常对象
*/
public ServiceException setMessage(String message) {
this.message = message;
return this;
}
/**
* 设置错误明细并返回当前异常对象。
*
* @param detailMessage 错误明细
* @return 当前异常对象
*/
public ServiceException setDetailMessage(String detailMessage) {
this.detailMessage = detailMessage;
return this;
}
}
@@ -0,0 +1,90 @@
package org.dromara.common.core.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
* SSE 场景专用异常。
*
* @author LionLi
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public final class SseException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 错误明细,内部调试错误
*/
private String detailMessage;
/**
* 使用错误消息构造 SSE 异常。
*
* @param message 错误消息
*/
public SseException(String message) {
this.message = message;
}
/**
* 使用错误消息和错误码构造 SSE 异常。
*
* @param message 错误消息
* @param code 错误码
*/
public SseException(String message, Integer code) {
this.message = message;
this.code = code;
}
/**
* 获取错误提示。
*
* @return 错误提示
*/
@Override
public String getMessage() {
return message;
}
/**
* 设置错误消息并返回当前异常对象。
*
* @param message 错误消息
* @return 当前异常对象
*/
public SseException setMessage(String message) {
this.message = message;
return this;
}
/**
* 设置错误明细并返回当前异常对象。
*
* @param detailMessage 错误明细
* @return 当前异常对象
*/
public SseException setDetailMessage(String detailMessage) {
this.detailMessage = detailMessage;
return this;
}
}
@@ -0,0 +1,103 @@
package org.dromara.common.core.exception.base;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import java.io.Serial;
/**
* 基础国际化异常,支持按错误码解析最终提示信息。
*
* @author ruoyi
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class BaseException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 所属模块
*/
private String module;
/**
* 错误码
*/
private String code;
/**
* 错误码对应的参数
*/
private Object[] args;
/**
* 错误消息
*/
private String defaultMessage;
/**
* 使用模块、错误码和参数构造异常。
*
* @param module 所属模块
* @param code 错误码
* @param args 参数
*/
public BaseException(String module, String code, Object[] args) {
this(module, code, args, null);
}
/**
* 使用模块和默认消息构造异常。
*
* @param module 所属模块
* @param defaultMessage 默认消息
*/
public BaseException(String module, String defaultMessage) {
this(module, null, null, defaultMessage);
}
/**
* 使用错误码和参数构造异常。
*
* @param code 错误码
* @param args 参数
*/
public BaseException(String code, Object[] args) {
this(null, code, args, null);
}
/**
* 使用默认消息构造异常。
*
* @param defaultMessage 默认消息
*/
public BaseException(String defaultMessage) {
this(null, null, null, defaultMessage);
}
/**
* 获取国际化后的错误消息。
*
* @return 错误消息
*/
@Override
public String getMessage() {
String message = null;
if (!StringUtils.isEmpty(code)) {
message = MessageUtils.message(code, args);
}
if (message == null) {
message = defaultMessage;
}
return message;
}
}
@@ -0,0 +1,27 @@
package org.dromara.common.core.exception.file;
import org.dromara.common.core.exception.base.BaseException;
import java.io.Serial;
/**
* 文件信息异常类
*
* @author ruoyi
*/
public class FileException extends BaseException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造文件异常。
*
* @param code 错误码
* @param args 错误码参数
*/
public FileException(String code, Object[] args) {
super("file", code, args, null);
}
}
@@ -0,0 +1,23 @@
package org.dromara.common.core.exception.file;
import java.io.Serial;
/**
* 文件名称超长限制异常类
*
* @author ruoyi
*/
public class FileNameLengthLimitExceededException extends FileException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造文件名称超长异常。
*
* @param defaultFileNameLength 默认文件名长度限制
*/
public FileNameLengthLimitExceededException(int defaultFileNameLength) {
super("upload.filename.exceed.length", new Object[]{defaultFileNameLength});
}
}
@@ -0,0 +1,23 @@
package org.dromara.common.core.exception.file;
import java.io.Serial;
/**
* 文件名大小限制异常类
*
* @author ruoyi
*/
public class FileSizeLimitExceededException extends FileException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造文件大小超限异常。
*
* @param defaultMaxSize 默认最大文件大小
*/
public FileSizeLimitExceededException(long defaultMaxSize) {
super("upload.exceed.maxSize", new Object[]{defaultMaxSize});
}
}
@@ -0,0 +1,21 @@
package org.dromara.common.core.exception.user;
import java.io.Serial;
/**
* 验证码错误异常类
*
* @author ruoyi
*/
public class CaptchaException extends UserException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造验证码错误异常。
*/
public CaptchaException() {
super("user.jcaptcha.error");
}
}
@@ -0,0 +1,21 @@
package org.dromara.common.core.exception.user;
import java.io.Serial;
/**
* 验证码失效异常类
*
* @author ruoyi
*/
public class CaptchaExpireException extends UserException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造验证码失效异常。
*/
public CaptchaExpireException() {
super("user.jcaptcha.expire");
}
}
@@ -0,0 +1,26 @@
package org.dromara.common.core.exception.user;
import org.dromara.common.core.exception.base.BaseException;
import java.io.Serial;
/**
* 用户信息异常类
*
* @author ruoyi
*/
public class UserException extends BaseException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造用户模块异常。
*
* @param code 错误码
* @param args 错误码参数
*/
public UserException(String code, Object... args) {
super("user", code, args, null);
}
}
@@ -0,0 +1,52 @@
package org.dromara.common.core.factory;
import cn.hutool.core.lang.PatternPool;
import org.dromara.common.core.constant.RegexConstants;
import java.util.regex.Pattern;
/**
* 正则表达式模式池工厂
* <p>初始化的时候将正则表达式加入缓存池当中</p>
* <p>提高正则表达式的性能,避免重复编译相同的正则表达式</p>
*
* @author 21001
*/
public class RegexPatternPoolFactory extends PatternPool {
/**
* 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
*/
public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);
/**
* 身份证号码(后6位)
*/
public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6);
/**
* QQ号码
*/
public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER);
/**
* 邮政编码
*/
public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE);
/**
* 注册账号
*/
public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT);
/**
* 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
*/
public static final Pattern PASSWORD = get(RegexConstants.PASSWORD);
/**
* 通用状态(0表示正常,1表示停用)
*/
public static final Pattern STATUS = get(RegexConstants.STATUS);
}
@@ -0,0 +1,41 @@
package org.dromara.common.core.factory;
import org.dromara.common.core.utils.StringUtils;
import org.jspecify.annotations.Nullable;
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;
import java.util.Objects;
/**
* yml 配置源工厂
*
* @author Lion Li
*/
public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
/**
* 创建配置属性源,支持将 yml/yaml 资源解析为 PropertiesPropertySource。
*
* @param name 属性源名称
* @param resource 配置资源
* @return 属性源
* @throws IOException 读取配置资源失败时抛出
*/
@Override
public PropertySource<?> createPropertySource(@Nullable 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, Objects.requireNonNull(factory.getObject()));
}
return super.createPropertySource(name, resource);
}
}
@@ -0,0 +1,87 @@
package org.dromara.common.core.service;
import org.dromara.common.core.domain.dto.DictDataDTO;
import org.dromara.common.core.domain.dto.DictTypeDTO;
import java.util.List;
import java.util.Map;
/**
* 通用 字典服务
*
* @author Lion Li
*/
public interface DictService {
/**
* 分隔符
*/
String SEPARATOR = ",";
/**
* 根据字典类型和字典值获取字典标签
*
* @param dictType 字典类型
* @param dictValue 字典值
* @return 字典标签
*/
default String getDictLabel(String dictType, String dictValue) {
return getDictLabel(dictType, dictValue, SEPARATOR);
}
/**
* 根据字典类型和字典标签获取字典值
*
* @param dictType 字典类型
* @param dictLabel 字典标签
* @return 字典值
*/
default String getDictValue(String dictType, String dictLabel) {
return getDictValue(dictType, dictLabel, SEPARATOR);
}
/**
* 根据字典类型和字典值获取字典标签
*
* @param dictType 字典类型
* @param dictValue 字典值
* @param separator 分隔符
* @return 字典标签
*/
String getDictLabel(String dictType, String dictValue, String separator);
/**
* 根据字典类型和字典标签获取字典值
*
* @param dictType 字典类型
* @param dictLabel 字典标签
* @param separator 分隔符
* @return 字典值
*/
String getDictValue(String dictType, String dictLabel, String separator);
/**
* 获取字典下所有的字典值与标签
*
* @param dictType 字典类型
* @return dictValue为keydictLabel为值组成的Map
*/
Map<String, String> getAllDictByDictType(String dictType);
/**
* 根据字典类型查询详细信息
*
* @param dictType 字典类型
* @return 字典类型详细信息
*/
DictTypeDTO getDictType(String dictType);
/**
* 根据字典类型查询字典数据列表
*
* @param dictType 字典类型
* @return 字典数据列表
*/
List<DictDataDTO> getDictData(String dictType);
}
@@ -0,0 +1,28 @@
package org.dromara.common.core.service;
import java.util.Set;
/**
* 用户权限处理
*
* @author Lion Li
*/
public interface PermissionService {
/**
* 获取角色数据权限
*
* @param userId 用户id
* @return 角色权限信息
*/
Set<String> getRolePermission(Long userId);
/**
* 获取菜单数据权限
*
* @param userId 用户id
* @return 菜单权限信息
*/
Set<String> getMenuPermission(Long userId);
}
@@ -0,0 +1,172 @@
package org.dromara.common.core.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* 时间工具类
*
* @author AprilWind
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DateUtils extends DateUtil {
/**
* 计算时间差并格式化(精确到秒)
*
* @param start 开始时间(支持 Date/LocalDateTime)
* @param end 结束时间(支持 Date/LocalDateTime)
* @return 时分秒格式时间差
*/
public static String formatBetweenBySecond(Object start, Object end) {
return formatTimeBetween(start, end, BetweenFormatter.Level.SECOND);
}
/**
* 通用时间差格式化
*
* @param startDate 开始时间
* @param endDate 结束时间
* @param level 精度级别
* @return 格式化时长
*/
public static String formatTimeBetween(Object startDate, Object endDate, BetweenFormatter.Level level) {
// 非空校验
Assert.notNull(startDate, "开始时间不能为空");
Assert.notNull(endDate, "结束时间不能为空");
Assert.notNull(level, "时间精度级别不能为空");
// 统一转为Date并校验格式合法性
Date start = Convert.toDate(startDate);
Date end = Convert.toDate(endDate);
Assert.notNull(start, "开始时间格式错误,无法解析为时间");
Assert.notNull(end, "结束时间格式错误,无法解析为时间");
long diffMillis = Math.abs(end.getTime() - start.getTime());
return formatBetween(diffMillis, level);
}
/**
* 校验日期范围及最大时间跨度
*
* @param startDate 开始日期
* @param endDate 结束日期
* @param maxValue 最大时间跨度限制值
* @param unit 时间单位
*/
public static void validateDateRange(Object startDate, Object endDate, int maxValue, TimeUnit unit) {
// 基础非空校验
Assert.notNull(startDate, "开始日期不能为空");
Assert.notNull(endDate, "结束日期不能为空");
Assert.notNull(unit, "时间单位不能为空");
// 统一转换并校验时间合法性
Date start = Convert.toDate(startDate);
Date end = Convert.toDate(endDate);
Assert.notNull(start, "开始日期格式错误,无法解析为时间");
Assert.notNull(end, "结束日期格式错误,无法解析为时间");
// 校验结束时间不能早于开始时间
if (end.before(start)) {
throw new ServiceException("结束日期不能早于开始日期");
}
long diffMillis = end.getTime() - start.getTime();
long diff = switch (unit) {
case DAYS -> TimeUnit.MILLISECONDS.toDays(diffMillis);
case HOURS -> TimeUnit.MILLISECONDS.toHours(diffMillis);
case MINUTES -> TimeUnit.MILLISECONDS.toMinutes(diffMillis);
default -> throw new IllegalArgumentException("不支持的时间单位:" + unit.name());
};
if (diff > maxValue) {
String msg = String.format("最大时间跨度为 %d %s", maxValue, unit.name().toLowerCase());
throw new ServiceException(msg);
}
}
/**
* 根据指定日期时间获取时间段(凌晨 / 上午 / 中午 / 下午 / 晚上)
*
* @param date 日期时间
* @return 时间段描述
*/
public static String getTodayHour(Date date) {
int hour = hour(date, true);
if (hour <= 6) {
return "凌晨";
} else if (hour < 12) {
return "上午";
} else if (hour == 12) {
return "中午";
} else if (hour <= 18) {
return "下午";
} else {
return "晚上";
}
}
/**
* 将日期格式化为仿微信的友好时间
* <p>
* 规则说明:
* 1. 未来时间:yyyy-MM-dd HH:mm
* 2. 今天:
* - 1 分钟内:刚刚
* - 1 小时内:X 分钟前
* - 超过 1 小时:凌晨/上午/中午/下午/晚上 HH:mm
* 3. 昨天:昨天 HH:mm
* 4. 本周:周X HH:mm
* 5. 今年内:MM-dd HH:mm
* 6. 非今年:yyyy-MM-dd HH:mm
*
* @param date 日期时间
* @return 格式化后的时间描述
*/
public static String formatFriendlyTime(Date date) {
if (date == null) {
return "";
}
Date now = new Date();
// 未来时间或非今年
if (date.after(now) || year(date) != year(now)) {
return formatDateTime(now);
}
// 今天
if (isSameDay(date, now)) {
long minutes = between(date, now, DateUnit.MINUTE);
if (minutes < 1) {
return "刚刚";
}
if (minutes < 60) {
return minutes + "分钟前";
}
return getTodayHour(date) + " " + format(date, "HH:mm");
}
// 昨天
if (isSameDay(date, yesterday())) {
return "昨天 " + format(date, "HH:mm");
}
// 本周
if (isSameWeek(date, now, true)) {
return dayOfWeekEnum(date).toChinese("") + " " + format(date, "HH:mm");
}
// 今年内其它时间
return format(date, "MM-dd HH:mm");
}
}
@@ -0,0 +1,87 @@
package org.dromara.common.core.utils;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* 脱敏工具类
*
* @author AprilWind
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DesensitizedUtils extends DesensitizedUtil {
/**
* 灵活脱敏方法
*
* @param value 原始字符串
* @param prefixVisible 前面可见长度
* @param suffixVisible 后面可见长度
* @param maskLength 中间掩码长度(固定显示多少 *,如果总长度不足则自动缩减)
* @return 脱敏后字符串
*/
public static String mask(String value, int prefixVisible, int suffixVisible, int maskLength) {
if (StrUtil.isBlank(value)) {
return value;
}
int len = value.length();
int prefixMaskLimit = prefixVisible + maskLength;
int fullLimit = prefixMaskLimit + suffixVisible;
// 规则 1:长度 <= 中间掩码长度 → 全掩码
if (len <= maskLength) {
return StrUtil.repeat('*', len);
}
String mask = StrUtil.repeat('*', maskLength);
// 规则 2:长度 <= 前缀 + 中间掩码
if (len <= prefixMaskLimit) {
return value.substring(0, len - maskLength) + mask;
}
String prefix = value.substring(0, prefixVisible);
// 规则 3:长度 <= 前缀 + 中间掩码 + 后缀
if (len <= fullLimit) {
int suffixLen = len - prefixMaskLimit;
return prefix + mask + value.substring(len - suffixLen);
}
// 规则 4:标准形态
return prefix + mask + value.substring(len - suffixVisible);
}
/**
* 高安全级别脱敏方法(Token / 私钥)
*
* @param value 原始字符串
* @param prefixVisible 前面可见长度(推荐0~4
* @param suffixVisible 后面可见长度(推荐0~4
* @return 脱敏后字符串
*/
public static String maskHighSecurity(String value, int prefixVisible, int suffixVisible) {
if (StrUtil.isBlank(value)) {
return value;
}
int len = value.length();
// 规则1:长度 <= 前缀可见长度 → 全部掩码
if (len <= prefixVisible) {
return StrUtil.repeat('*', len);
}
// 规则2:长度 <= 前缀 + 后缀可见长度 → 优先掩码后面
if (len <= prefixVisible + suffixVisible) {
return value.substring(0, len - prefixVisible) + StrUtil.repeat('*', prefixVisible);
}
// 规则3:标准形态 → 前后可见,中间全部掩码
return value.substring(0, prefixVisible)
+ StrUtil.repeat('*', len - prefixVisible - suffixVisible)
+ value.substring(len - suffixVisible);
}
}
@@ -0,0 +1,92 @@
package org.dromara.common.core.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import io.github.linpeilie.Converter;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* Mapstruct 工具类
* <p>参考文档:<a href="https://mapstruct.plus/introduction/quick-start.html">mapstruct-plus</a></p>
*
* @author Michelle.Chung
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MapstructUtils {
private final static Converter CONVERTER = SpringUtils.getBean(Converter.class);
/**
* 将 T 类型对象,转换为 desc 类型的对象并返回
*
* @param source 数据来源实体
* @param desc 描述对象 转换后的对象
* @return desc
*/
public static <T, V> V convert(T source, Class<V> desc) {
if (ObjectUtil.isNull(source)) {
return null;
}
if (ObjectUtil.isNull(desc)) {
return null;
}
return CONVERTER.convert(source, desc);
}
/**
* 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象
*
* @param source 数据来源实体
* @param desc 转换后的对象
* @return desc
*/
public static <T, V> V convert(T source, V desc) {
if (ObjectUtil.isNull(source)) {
return null;
}
if (ObjectUtil.isNull(desc)) {
return null;
}
return CONVERTER.convert(source, desc);
}
/**
* 将 T 类型的集合,转换为 desc 类型的集合并返回
*
* @param sourceList 数据来源实体列表
* @param desc 描述对象 转换后的对象
* @return desc
*/
public static <T, V> List<V> convert(List<T> sourceList, Class<V> desc) {
if (ObjectUtil.isNull(sourceList)) {
return null;
}
if (CollUtil.isEmpty(sourceList)) {
return CollUtil.newArrayList();
}
return CONVERTER.convert(sourceList, desc);
}
/**
* 将 Map 转换为 beanClass 类型的集合并返回
*
* @param map 数据来源
* @param beanClass bean类
* @return bean对象
*/
public static <T> T convert(Map<String, Object> map, Class<T> beanClass) {
if (MapUtil.isEmpty(map)) {
return null;
}
if (ObjectUtil.isNull(beanClass)) {
return null;
}
return CONVERTER.convert(map, beanClass);
}
}
@@ -0,0 +1,33 @@
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;
/**
* 获取i18n资源文件
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MessageUtils {
private static final MessageSource MESSAGE_SOURCE = SpringUtils.getBean(MessageSource.class);
/**
* 根据消息键和参数 获取消息 委托给spring messageSource
*
* @param code 消息键
* @param args 参数
* @return 获取国际化翻译值
*/
public static String message(String code, Object... args) {
try {
return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
} catch (NoSuchMessageException e) {
return code;
}
}
}
@@ -0,0 +1,151 @@
package org.dromara.common.core.utils;
import cn.hutool.core.lang.PatternPool;
import cn.hutool.core.net.NetUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.regex.RegexUtils;
import java.math.BigInteger;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 增强网络相关工具类
*
* @author 秋辞未寒
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class NetUtils extends NetUtil {
/**
* 判断是否为IPv6地址
*
* @param ip IP地址
* @return 是否为IPv6地址
*/
public static boolean isIPv6(String ip) {
try {
// 判断是否为IPv6地址
return InetAddress.getByName(ip) instanceof Inet6Address;
} catch (UnknownHostException e) {
return false;
}
}
/**
* 判断IPv6地址是否为内网地址
* <br><br>
* 以下地址将归类为本地地址,如有业务场景有需要,请根据需求自行处理:
* <pre>
* 通配符地址 0:0:0:0:0:0:0:0
* 链路本地地址 fe80::/10
* 唯一本地地址 fec0::/10
* 环回地址 ::1
* </pre>
*
* @param ip IP地址
* @return 是否为内网地址
*/
public static boolean isInnerIPv6(String ip) {
try {
// 判断是否为IPv6地址
InetAddress inetAddress = InetAddress.getByName(ip);
if (inetAddress instanceof Inet6Address inet6Address) {
// isAnyLocalAddress 判断是否为通配符地址,通常不会将其视为内网地址,根据业务场景自行处理判断
// isLinkLocalAddress 判断是否为链路本地地址,通常不算内网地址,是否划分归属于内网需要根据业务场景自行处理判断
// isLoopbackAddress 判断是否为环回地址,与IPv4的 127.0.0.1 同理,用于表示本机
// isSiteLocalAddress 判断是否为本地站点地址,IPv6唯一本地地址(Unique Local Addresses,简称ULA
if (inet6Address.isAnyLocalAddress()
|| inet6Address.isLinkLocalAddress()
|| inet6Address.isLoopbackAddress()
|| inet6Address.isSiteLocalAddress()) {
return true;
}
}
} catch (UnknownHostException e) {
// 注意,isInnerIPv6方法和isIPv6方法的适用范围不同,所以此处不能忽略其异常信息。
throw new IllegalArgumentException("Invalid IPv6 address!", e);
}
return false;
}
/**
* 判断是否为IPv4地址
*
* @param ip IP地址
* @return 是否为IPv4地址
*/
public static boolean isIPv4(String ip) {
return RegexUtils.isMatch(PatternPool.IPV4, ip);
}
/**
* 匹配 IP 规则,支持精确值、通配符与 CIDR。
*
* @param rule IP 规则
* @param clientIp 客户端 IP
* @return 是否匹配
*/
public static boolean isMatchIpRule(String rule, String clientIp) {
if (StringUtils.isBlank(rule) || StringUtils.isBlank(clientIp)) {
return false;
}
String ipRule = StringUtils.trim(rule);
if (StringUtils.equals(ipRule, clientIp)) {
return true;
}
if (ipRule.contains("/")) {
return isMatchCidr(ipRule, clientIp);
}
if (StringUtils.containsAny(ipRule, "*", "?")) {
String regex = ipRule
.replace(".", "\\.")
.replace("*", ".*")
.replace("?", ".");
return clientIp.matches(regex);
}
return false;
}
/**
* 匹配 CIDR 网段。
*
* @param cidr CIDR 规则
* @param clientIp 客户端 IP
* @return 是否命中
*/
public static boolean isMatchCidr(String cidr, String clientIp) {
try {
String[] parts = cidr.split("/");
if (parts.length != 2) {
return false;
}
InetAddress networkAddress = InetAddress.getByName(parts[0]);
InetAddress currentAddress = InetAddress.getByName(clientIp);
byte[] networkBytes = networkAddress.getAddress();
byte[] currentBytes = currentAddress.getAddress();
if (networkBytes.length != currentBytes.length) {
return false;
}
int prefixLength = Integer.parseInt(parts[1]);
int maxPrefix = networkBytes.length * 8;
if (prefixLength < 0 || prefixLength > maxPrefix) {
return false;
}
BigInteger mask = prefixLength == 0
? BigInteger.ZERO
: BigInteger.ONE.shiftLeft(prefixLength).subtract(BigInteger.ONE).shiftLeft(maxPrefix - prefixLength);
BigInteger network = new BigInteger(1, networkBytes);
BigInteger current = new BigInteger(1, currentBytes);
return network.and(mask).equals(current.and(mask));
} catch (UnknownHostException | NumberFormatException e) {
log.debug("IP白名单CIDR规则解析失败: {}", cidr, e);
return false;
}
}
}
@@ -0,0 +1,60 @@
package org.dromara.common.core.utils;
import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.function.Function;
/**
* 对象工具类
*
* @author 秋辞未寒
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ObjectUtils extends ObjectUtil {
/**
* 如果对象不为空,则获取对象中的某个字段 ObjectUtils.notNullGetter(user, User::getName);
*
* @param obj 对象
* @param func 获取方法
* @return 对象字段
*/
public static <T, E> E notNullGetter(T obj, Function<T, E> func) {
if (isNotNull(obj) && isNotNull(func)) {
return func.apply(obj);
}
return null;
}
/**
* 如果对象不为空,则获取对象中的某个字段,否则返回默认值
*
* @param obj 对象
* @param func 获取方法
* @param defaultValue 默认值
* @return 对象字段
*/
public static <T, E> E notNullGetter(T obj, Function<T, E> func, E defaultValue) {
if (isNotNull(obj) && isNotNull(func)) {
return func.apply(obj);
}
return defaultValue;
}
/**
* 如果值不为空,则返回值,否则返回默认值
*
* @param obj 对象
* @param defaultValue 默认值
* @return 对象字段
*/
public static <T> T notNull(T obj, T defaultValue) {
if (isNotNull(obj)) {
return obj;
}
return defaultValue;
}
}
@@ -0,0 +1,307 @@
package org.dromara.common.core.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.http.HttpStatus;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
import java.io.IOException;
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;
/**
* 客户端工具类,提供获取请求参数、响应处理、头部信息等常用操作
*
* @author ruoyi
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j
public class ServletUtils extends JakartaServletUtil {
/**
* 获取指定名称的 String 类型的请求参数
*
* @param name 参数名
* @return 参数值
*/
public static String getParameter(String name) {
return getRequest().getParameter(name);
}
/**
* 获取指定名称的 String 类型的请求参数,若参数不存在,则返回默认值
*
* @param name 参数名
* @param defaultValue 默认值
* @return 参数值或默认值
*/
public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue);
}
/**
* 获取指定名称的 Integer 类型的请求参数
*
* @param name 参数名
* @return 参数值
*/
public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name));
}
/**
* 获取指定名称的 Integer 类型的请求参数,若参数不存在,则返回默认值
*
* @param name 参数名
* @param defaultValue 默认值
* @return 参数值或默认值
*/
public static Integer getParameterToInt(String name, Integer defaultValue) {
return Convert.toInt(getRequest().getParameter(name), defaultValue);
}
/**
* 获取指定名称的 Boolean 类型的请求参数
*
* @param name 参数名
* @return 参数值
*/
public static Boolean getParameterToBool(String name) {
return Convert.toBool(getRequest().getParameter(name));
}
/**
* 获取指定名称的 Boolean 类型的请求参数,若参数不存在,则返回默认值
*
* @param name 参数名
* @param defaultValue 默认值
* @return 参数值或默认值
*/
public static Boolean getParameterToBool(String name, Boolean defaultValue) {
return Convert.toBool(getRequest().getParameter(name), defaultValue);
}
/**
* 获取所有请求参数(以 Map 的形式返回)
*
* @param request 请求对象{@link ServletRequest}
* @return 请求参数的 Map,键为参数名,值为参数值数组
*/
public static Map<String, String[]> getParams(ServletRequest request) {
final Map<String, String[]> map = request.getParameterMap();
return Collections.unmodifiableMap(map);
}
/**
* 获取所有请求参数(以 Map 的形式返回,值为字符串形式的拼接)
*
* @param request 请求对象{@link ServletRequest}
* @return 请求参数的 Map,键为参数名,值为拼接后的字符串
*/
public static Map<String, String> getParamMap(ServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
params.put(entry.getKey(), StringUtils.joinComma(entry.getValue()));
}
return params;
}
/**
* 获取当前 HTTP 请求对象
*
* @return 当前 HTTP 请求对象
*/
public static HttpServletRequest getRequest() {
try {
return getRequestAttributes().getRequest();
} catch (Exception e) {
return null;
}
}
/**
* 获取当前 HTTP 响应对象
*
* @return 当前 HTTP 响应对象
*/
public static HttpServletResponse getResponse() {
try {
return getRequestAttributes().getResponse();
} catch (Exception e) {
return null;
}
}
/**
* 获取当前请求的 HttpSession 对象
* <p>
* 如果当前请求已经关联了一个会话(即已经存在有效的 session ID),
* 则返回该会话对象;如果没有关联会话,则会创建一个新的会话对象并返回。
* <p>
* HttpSession 用于存储会话级别的数据,如用户登录信息、购物车内容等,
* 可以在多个请求之间共享会话数据
*
* @return 当前请求的 HttpSession 对象
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
/**
* 获取当前请求的请求属性
*
* @return {@link ServletRequestAttributes} 请求属性对象
*/
public static ServletRequestAttributes getRequestAttributes() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
} catch (Exception e) {
return null;
}
}
/**
* 获取指定请求头的值,如果头部为空则返回空字符串
*
* @param request 请求对象
* @param name 头部名称
* @return 头部值
*/
public static String getHeader(HttpServletRequest request, String name) {
String value = request.getHeader(name);
if (StringUtils.isEmpty(value)) {
return StringUtils.EMPTY;
}
return urlDecode(value);
}
/**
* 获取所有请求头的 Map,键为头部名称,值为头部值
*
* @param request 请求对象
* @return 请求头的 Map
*/
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;
}
/**
* 将字符串渲染到客户端(以 JSON 格式返回)
*
* @param response 渲染对象
* @param string 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(HttpStatus.HTTP_OK);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().print(string);
} catch (IOException e) {
log.error("渲染响应内容异常", e);
}
}
/**
* 判断当前请求是否为 Ajax 异步请求
*
* @param request 请求对象
* @return 是否为 Ajax 请求
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
// 判断 Accept 头部是否包含 application/json
String accept = request.getHeader("accept");
if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
return true;
}
// 判断 X-Requested-With 头部是否包含 XMLHttpRequest
String xRequestedWith = request.getHeader("X-Requested-With");
if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
return true;
}
// 判断 URI 后缀是否为 .json 或 .xml
String uri = request.getRequestURI();
if (StringUtils.endsWithAny(StringUtils.lowerCase(uri), ".json", ".xml")) {
return true;
}
// 判断请求参数 __ajax 是否为 json 或 xml
String ajax = request.getParameter("__ajax");
return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
}
/**
* 获取客户端 IP 地址
*
* @return 客户端 IP 地址
*/
public static String getClientIP() {
return getClientIP(getRequest());
}
/**
* 获取客户端 IP 地址
*
* @return 客户端 IP 地址
*/
public static String getClientIP(HttpServletRequest request, String... otherHeaderNames) {
String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
headers = ArrayUtil.addAll(otherHeaderNames, headers);
}
String ip = getClientIPByHeader(request, headers);
return StringUtils.strip(ip, "[]");
}
/**
* 对内容进行 URL 编码
*
* @param str 内容
* @return 编码后的内容
*/
public static String urlEncode(String str) {
return URLEncoder.encode(str, StandardCharsets.UTF_8);
}
/**
* 对内容进行 URL 解码
*
* @param str 内容
* @return 解码后的内容
*/
public static String urlDecode(String str) {
return URLDecoder.decode(str, StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,72 @@
package org.dromara.common.core.utils;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.thread.Threading;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
* spring工具类
*
* @author Lion Li
*/
@Component
public final class SpringUtils extends SpringUtil {
/**
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
*/
public static boolean containsBean(String name) {
return getBeanFactory().containsBean(name);
}
/**
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
* 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException
*/
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().isSingleton(name);
}
/**
* @return Class 注册对象的类型
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().getType(name);
}
/**
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
*/
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().getAliases(name);
}
/**
* 获取aop代理对象
*/
@SuppressWarnings("unchecked")
public static <T> T getAopProxy(T invoker) {
return (T) getBean(invoker.getClass());
}
/**
* 获取spring上下文
*/
public static ApplicationContext context() {
return getApplicationContext();
}
/**
* 当前是否启用虚拟线程。
*
* @return true 启用 false 未启用
*/
public static boolean isVirtual() {
return Threading.VIRTUAL.isActive(getBean(Environment.class));
}
}
@@ -0,0 +1,329 @@
package org.dromara.common.core.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* stream 流工具类
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class StreamUtils {
/**
* 将collection过滤
*
* @param collection 需要转化的集合
* @param function 过滤方法
* @return 过滤后的list
*/
public static <E> List<E> filter(Collection<E> collection, Predicate<E> function) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
return collection.stream()
.filter(function)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList());
}
/**
* 找到流中满足条件的第一个元素
*
* @param collection 需要查询的集合
* @param function 过滤方法
* @return 找到符合条件的第一个元素,没有则返回 Optional.empty()
*/
public static <E> Optional<E> findFirst(Collection<E> collection, Predicate<E> function) {
if (CollUtil.isEmpty(collection)) {
return Optional.empty();
}
return collection.stream()
.filter(function)
.findFirst();
}
/**
* 找到流中满足条件的第一个元素值
*
* @param collection 需要查询的集合
* @param function 过滤方法
* @return 找到符合条件的第一个元素,没有则返回 null
*/
public static <E> E findFirstValue(Collection<E> collection, Predicate<E> function) {
return findFirst(collection, function).orElse(null);
}
/**
* 找到流中任意一个满足条件的元素
*
* @param collection 需要查询的集合
* @param function 过滤方法
* @return 找到符合条件的任意一个元素,没有则返回 Optional.empty()
*/
public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
if (CollUtil.isEmpty(collection)) {
return Optional.empty();
}
return collection.stream()
.filter(function)
.findAny();
}
/**
* 找到流中任意一个满足条件的元素值
*
* @param collection 需要查询的集合
* @param function 过滤方法
* @return 找到符合条件的任意一个元素,没有则返回null
*/
public static <E> E findAnyValue(Collection<E> collection, Predicate<E> function) {
return findAny(collection, function).orElse(null);
}
/**
* 将collection拼接
*
* @param collection 需要转化的集合
* @param function 拼接方法
* @return 拼接后的list
*/
public static <E> String join(Collection<E> collection, Function<E, String> function) {
return join(collection, function, StringUtils.SEPARATOR);
}
/**
* 将collection拼接
*
* @param collection 需要转化的集合
* @param function 拼接方法
* @param delimiter 拼接符
* @return 拼接后的list
*/
public static <E> String join(Collection<E> collection, Function<E, String> function, CharSequence delimiter) {
if (CollUtil.isEmpty(collection)) {
return StringUtils.EMPTY;
}
return collection.stream()
.map(function)
.filter(Objects::nonNull)
.collect(Collectors.joining(delimiter));
}
/**
* 将collection排序
*
* @param collection 需要转化的集合
* @param comparing 排序方法
* @return 排序后的list
*/
public static <E> List<E> sorted(Collection<E> collection, Comparator<E> comparing) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
return collection.stream()
.filter(Objects::nonNull)
.sorted(comparing)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList());
}
/**
* 将collection转化为类型不变的map<br>
* <B>{@code Collection<V> ----> Map<K,V>}</B>
*
* @param collection 需要转化的集合
* @param key V类型转化为K类型的lambda方法
* @param <V> collection中的泛型
* @param <K> map中的key类型
* @return 转化后的map
*/
public static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> key) {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
return collection.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
}
/**
* 将Collection转化为map(value类型与collection的泛型不同)<br>
* <B>{@code Collection<E> -----> Map<K,V> }</B>
*
* @param collection 需要转化的集合
* @param key E类型转化为K类型的lambda方法
* @param value E类型转化为V类型的lambda方法
* @param <E> collection中的泛型
* @param <K> map中的key类型
* @param <V> map中的value类型
* @return 转化后的map
*/
public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> value) {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
return collection.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(key, value, (l, r) -> l));
}
/**
* 获取 map 中的数据作为新 Map 的 value key 不变
*
* @param map 需要处理的map
* @param take 取值函数
* @param <K> map中的key类型
* @param <E> map中的value类型
* @param <V> 新map中的value类型
* @return 新的map
*/
public static <K, E, V> Map<K, V> toMap(Map<K, E> map, BiFunction<K, E, V> take) {
if (CollUtil.isEmpty(map)) {
return MapUtil.newHashMap();
}
return toMap(map.entrySet(), Map.Entry::getKey, entry -> take.apply(entry.getKey(), entry.getValue()));
}
/**
* 将collection按照规则(比如有相同的班级id)分类成map<br>
* <B>{@code Collection<E> -------> Map<K,List<E>> } </B>
*
* @param collection 需要分类的集合
* @param key 分类的规则
* @param <E> collection中的泛型
* @param <K> map中的key类型
* @return 分类后的map
*/
public static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> key) {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
return collection.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
}
/**
* 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
* <B>{@code Collection<E> ---> Map<T,Map<U,List<E>>> } </B>
*
* @param collection 需要分类的集合
* @param key1 第一个分类的规则
* @param key2 第二个分类的规则
* @param <E> 集合元素类型
* @param <K> 第一个map中的key类型
* @param <U> 第二个map中的key类型
* @return 分类后的map
*/
public static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> key2) {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
return collection.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
}
/**
* 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
* <B>{@code Collection<E> ---> Map<T,Map<U,E>> } </B>
*
* @param collection 需要分类的集合
* @param key1 第一个分类的规则
* @param key2 第二个分类的规则
* @param <T> 第一个map中的key类型
* @param <U> 第二个map中的key类型
* @param <E> collection中的泛型
* @return 分类后的map
*/
public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
return collection.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
}
/**
* 将collection转化为List集合,但是两者的泛型不同<br>
* <B>{@code Collection<E> ------> List<T> } </B>
*
* @param collection 需要转化的集合
* @param function collection中的泛型转化为list泛型的lambda表达式
* @param <E> collection中的泛型
* @param <T> List中的泛型
* @return 转化后的list
*/
public static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
return collection.stream()
.map(function)
.filter(Objects::nonNull)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList());
}
/**
* 将collection转化为Set集合,但是两者的泛型不同<br>
* <B>{@code Collection<E> ------> Set<T> } </B>
*
* @param collection 需要转化的集合
* @param function collection中的泛型转化为set泛型的lambda表达式
* @param <E> collection中的泛型
* @param <T> Set中的泛型
* @return 转化后的Set
*/
public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newHashSet();
}
return collection.stream()
.map(function)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
/**
* 合并两个相同key类型的map
*
* @param map1 第一个需要合并的 map
* @param map2 第二个需要合并的 map
* @param merge 合并的lambda,将key value1 value2合并成最终的类型,注意value可能为空的情况
* @param <K> map中的key类型
* @param <X> 第一个 map的value类型
* @param <Y> 第二个 map的value类型
* @param <V> 最终map的value类型
* @return 合并后的map
*/
public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
if (CollUtil.isEmpty(map1) && CollUtil.isEmpty(map2)) {
// 如果两个 map 都为空,则直接返回空的 map
return MapUtil.newHashMap();
} else if (CollUtil.isEmpty(map1)) {
// 如果 map1 为空,则直接处理返回 map2
return toMap(map2.entrySet(), Map.Entry::getKey, entry -> merge.apply(null, entry.getValue()));
} else if (CollUtil.isEmpty(map2)) {
// 如果 map2 为空,则直接处理返回 map1
return toMap(map1.entrySet(), Map.Entry::getKey, entry -> merge.apply(entry.getValue(), null));
}
Set<K> keySet = new HashSet<>();
keySet.addAll(map1.keySet());
keySet.addAll(map2.keySet());
return toMap(keySet, key -> key, key -> merge.apply(map1.get(key), map2.get(key)));
}
}
@@ -0,0 +1,575 @@
package org.dromara.common.core.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import org.apache.commons.lang3.Strings;
import org.springframework.util.AntPathMatcher;
import java.nio.charset.Charset;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 字符串工具类
*
* @author Lion Li
*/
public class StringUtils extends org.apache.commons.lang3.StringUtils {
public static final String SEPARATOR = ",";
public static final String SLASH = "/";
public static final String COLON = ":";
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
@Deprecated
private StringUtils() {
}
/**
* 获取参数不为空值
*
* @param str defaultValue 要判断的value
* @return value 返回值
*/
public static String blankToDefault(String str, String defaultValue) {
return StrUtil.blankToDefault(str, defaultValue);
}
/**
* * 判断一个字符串是否为空串
*
* @param str String
* @return true:为空 false:非空
*/
public static boolean isEmpty(String str) {
return StrUtil.isEmpty(str);
}
/**
* * 判断一个字符串是否为非空串
*
* @param str String
* @return true:非空串 false:空串
*/
public static boolean isNotEmpty(String str) {
return !isEmpty(str);
}
/**
* 去空格
*/
public static String trim(String str) {
return StrUtil.trim(str);
}
/**
* 截取字符串
*
* @param str 字符串
* @param start 开始
* @return 结果
*/
public static String substring(final String str, int start) {
return substring(str, start, str.length());
}
/**
* 截取字符串
*
* @param str 字符串
* @param start 开始
* @param end 结束
* @return 结果
*/
public static String substring(final String str, int start, int end) {
return StrUtil.sub(str, start, end);
}
/**
* 格式化文本, {} 表示占位符<br>
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
* 例:<br>
* 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
* 转义{} format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
* 转义\ format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
*
* @param template 文本模板,被替换的部分用 {} 表示
* @param params 参数值
* @return 格式化后的文本
*/
public static String format(String template, Object... params) {
return StrUtil.format(template, params);
}
/**
* 是否为http(s)://开头
*
* @param link 链接
* @return 结果
*/
public static boolean ishttp(String link) {
return Validator.isUrl(link);
}
/**
* 字符串转set
*
* @param str 字符串
* @param sep 分隔符
* @return set集合
*/
public static Set<String> str2Set(String str, String sep) {
return new HashSet<>(str2List(str, sep, true, false));
}
/**
* 字符串转list
*
* @param str 字符串
* @param sep 分隔符
* @param filterBlank 过滤纯空白
* @param trim 去掉首尾空白
* @return list集合
*/
public static List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
List<String> list = new ArrayList<>();
if (isEmpty(str)) {
return list;
}
// 过滤空白字符串
if (filterBlank && isBlank(str)) {
return list;
}
String[] split = str.split(sep);
for (String string : split) {
if (filterBlank && isBlank(string)) {
continue;
}
if (trim) {
string = trim(string);
}
list.add(string);
}
return list;
}
/**
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
*
* @param cs 指定字符串
* @param searchCharSequences 需要检查的字符串数组
* @return 是否包含任意一个字符串
*/
public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
}
/**
* 驼峰转下划线命名
*/
public static String toUnderScoreCase(String str) {
return StrUtil.toUnderlineCase(str);
}
/**
* 是否包含字符串
*
* @param str 验证字符串
* @param strs 字符串组
* @return 包含返回true
*/
public static boolean inStringIgnoreCase(String str, String... strs) {
return StrUtil.equalsAnyIgnoreCase(str, strs);
}
/**
* 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
*
* @param name 转换前的下划线大写方式命名的字符串
* @return 转换后的驼峰式命名的字符串
*/
public static String convertToCamelCase(String name) {
return StrUtil.upperFirst(StrUtil.toCamelCase(name));
}
/**
* 驼峰式命名法 例如:user_name->userName
*/
public static String toCamelCase(String s) {
return StrUtil.toCamelCase(s);
}
/**
* 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
*
* @param str 指定字符串
* @param strs 需要检查的字符串数组
* @return 是否匹配
*/
public static boolean matches(String str, List<String> strs) {
if (isEmpty(str) || CollUtil.isEmpty(strs)) {
return false;
}
for (String pattern : strs) {
if (isMatch(pattern, str)) {
return true;
}
}
return false;
}
/**
* 判断url是否与规则配置:
* ? 表示单个字符;
* * 表示一层路径内的任意字符串,不可跨层级;
* ** 表示任意层路径;
*
* @param pattern 匹配规则
* @param url 需要匹配的url
*/
public static boolean isMatch(String pattern, String url) {
return ANT_PATH_MATCHER.match(pattern, url);
}
/**
* 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
*
* @param num 数字对象
* @param size 字符串指定长度
* @return 返回数字的字符串格式,该字符串为指定长度。
*/
public static String padl(final Number num, final int size) {
return padl(num.toString(), size, '0');
}
/**
* 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
*
* @param s 原始字符串
* @param size 字符串指定长度
* @param c 用于补齐的字符
* @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
*/
public static String padl(final String s, final int size, final char c) {
final StringBuilder sb = new StringBuilder(size);
if (s != null) {
final int len = s.length();
if (s.length() <= size) {
sb.append(Convert.toStr(c).repeat(size - len));
sb.append(s);
} else {
return s.substring(len - size, len);
}
} else {
sb.append(Convert.toStr(c).repeat(Math.max(0, size)));
}
return sb.toString();
}
/**
* 切分字符串(分隔符默认逗号)
*
* @param str 被切分的字符串
* @return 分割后的数据列表
*/
public static List<String> splitList(String str) {
return splitTo(str, Convert::toStr);
}
/**
* 切分字符串
*
* @param str 被切分的字符串
* @param separator 分隔符
* @return 分割后的数据列表
*/
public static List<String> splitList(String str, String separator) {
return splitTo(str, separator, Convert::toStr);
}
/**
* 切分字符串自定义转换(分隔符默认逗号)
*
* @param str 被切分的字符串
* @param mapper 自定义转换
* @return 分割后的数据列表
*/
public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
return splitTo(str, SEPARATOR, mapper);
}
/**
* 切分字符串自定义转换
*
* @param str 被切分的字符串
* @param separator 分隔符
* @param mapper 自定义转换
* @return 分割后的数据列表
*/
public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
if (isBlank(str)) {
return new ArrayList<>(0);
}
return StrUtil.split(str, separator)
.stream()
.filter(Objects::nonNull)
.map(mapper)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 不区分大小写检查 CharSequence 是否以指定的前缀开头。
*
* @param str 要检查的 CharSequence 可能为 null
* @param prefixs 要查找的前缀可能为 null
* @return 是否包含
*/
public static boolean startWithAnyIgnoreCase(CharSequence str, CharSequence... prefixs) {
// 判断是否是以指定字符串开头
for (CharSequence prefix : prefixs) {
if (Strings.CI.startsWith(str, prefix)) {
return true;
}
}
return false;
}
/**
* 将字符串从源字符集转换为目标字符集
*
* @param input 原始字符串
* @param fromCharset 源字符集
* @param toCharset 目标字符集
* @return 转换后的字符串
*/
public static String convert(String input, Charset fromCharset, Charset toCharset) {
if (isBlank(input)) {
return input;
}
try {
// 从源字符集获取字节
byte[] bytes = input.getBytes(fromCharset);
// 使用目标字符集解码
return new String(bytes, toCharset);
} catch (Exception e) {
return input;
}
}
/**
* 将可迭代对象中的元素使用逗号拼接成字符串
*
* @param iterable 可迭代对象,如 List、Set 等
* @return 拼接后的字符串
*/
public static String joinComma(Iterable<?> iterable) {
return StringUtils.join(iterable, SEPARATOR);
}
/**
* 将数组中的元素使用逗号拼接成字符串
*
* @param array 任意类型的数组
* @return 拼接后的字符串
*/
public static String joinComma(Object[] array) {
return StringUtils.join(array, SEPARATOR);
}
/**
* 判断两个字符串是否相等
*
* @param cs1 字符串1
* @param cs2 字符串2
* @return 是否相等
*/
public static boolean equals(final CharSequence cs1, final CharSequence cs2) {
return Strings.CS.equals(cs1, cs2);
}
/**
* 判断字符串是否在指定的字符串列表中
*
* @param string 字符串
* @param searchStrings 字符串列表
* @return 是否在列表中
*/
public static boolean equalsAny(final CharSequence string, final CharSequence... searchStrings) {
return Strings.CS.equalsAny(string, searchStrings);
}
/**
* 忽略大小写判断字符串是否在指定的字符串列表中
*
* @param string 字符串
* @param searchStrings 字符串列表
* @return 是否在列表中
*/
public static boolean equalsAnyIgnoreCase(final CharSequence string, final CharSequence... searchStrings) {
return Strings.CI.equalsAny(string, searchStrings);
}
/**
* 忽略大小写判断两个字符串是否相等
*
* @param cs1 字符串1
* @param cs2 字符串2
* @return 是否相等
*/
public static boolean equalsIgnoreCase(final CharSequence cs1, final CharSequence cs2) {
return Strings.CI.equals(cs1, cs2);
}
/**
* 检查指定的字符序列中是否包含另一个字符序列。
*
* @param seq 要检查的字符序列,不能为null
* @param searchSeq 要搜索的字符序列,不能为null
* @return 如果seq中包含searchSeq,则返回true;否则返回false
*/
public static boolean contains(final CharSequence seq, final CharSequence searchSeq) {
return Strings.CS.contains(seq, searchSeq);
}
/**
* 忽略大小写检查指定字符序列中是否包含另一个字符序列。
*
* @param seq 要检查的字符序列
* @param searchSeq 要搜索的字符序列
* @return 如果包含则返回 true,否则返回 false
*/
public static boolean containsIgnoreCase(final CharSequence seq, final CharSequence searchSeq) {
return Strings.CI.contains(seq, searchSeq);
}
/**
* 检查 CharSequence 是否以指定前缀开头。
*
* @param str 要检查的字符序列
* @param prefix 要查找的前缀
* @return 如果以指定前缀开头则返回 true,否则返回 false
*/
public static boolean startsWith(final CharSequence str, final CharSequence prefix) {
return Strings.CS.startsWith(str, prefix);
}
/**
* 忽略大小写检查 CharSequence 是否以指定前缀开头。
*
* @param str 要检查的字符序列
* @param prefix 要查找的前缀
* @return 如果以指定前缀开头则返回 true,否则返回 false
*/
public static boolean startsWithIgnoreCase(final CharSequence str, final CharSequence prefix) {
return Strings.CI.startsWith(str, prefix);
}
/**
* 忽略大小写检查 CharSequence 是否以指定后缀结尾。
*
* @param str 要检查的字符序列
* @param suffix 要查找的后缀
* @return 如果以指定后缀结尾则返回 true,否则返回 false
*/
public static boolean endsWithIgnoreCase(final CharSequence str, final CharSequence suffix) {
return Strings.CI.endsWith(str, suffix);
}
/**
* 返回指定字符序列首次出现的位置。
*
* @param seq 源字符序列
* @param searchSeq 待查找字符序列
* @return 首次出现的位置,不存在时返回 -1
*/
public static int indexOf(final CharSequence seq, final CharSequence searchSeq) {
if (seq == null || searchSeq == null) {
return -1;
}
return seq.toString().indexOf(searchSeq.toString());
}
/**
* 移除字符串中的指定字符序列。
*
* @param str 要处理的字符串,不能为null
* @param remove 要移除的字符序列,不能为null
* @return 处理后的字符串
*/
public static String remove(final String str, final String remove) {
return Strings.CS.remove(str, remove);
}
/**
* 如果字符串以指定前缀开头,则移除该前缀。
*
* @param str 要处理的字符串
* @param remove 要移除的前缀
* @return 处理后的字符串
*/
public static String removeStart(final String str, final String remove) {
if (isEmpty(str) || isEmpty(remove)) {
return str;
}
return startsWith(str, remove) ? str.substring(remove.length()) : str;
}
/**
* 替换字符串中的目标子串。
*
* @param text 原始字符串
* @param searchString 需要替换的子串
* @param replacement 替换后的子串
* @return 替换后的字符串
*/
public static String replace(final String text, final String searchString, final String replacement) {
if (text == null || isEmpty(searchString) || replacement == null) {
return text;
}
return text.replace(searchString, replacement);
}
/**
* 检查字符串是否包含任意一个指定的字符序列
*
* @param cs 要检查的字符串
* @param searchCharSequences 需要查找的字符序列数组
* @return 如果包含任意一个字符序列返回 true,否则返回 false
*/
public static boolean containsAny(final CharSequence cs, final CharSequence... searchCharSequences) {
return Strings.CS.containsAny(cs, searchCharSequences);
}
/**
* 将一个字符串替换为较大字符串内的另一个字符串,一次
*/
public static String replaceOnce(final String text, final String searchString, final String replacement) {
return Strings.CS.replaceOnce(text, searchString, replacement);
}
/**
* 测试 CharSequence 是否以所提供的任何后缀结尾。
*/
public static boolean endsWithAny(final CharSequence sequence, final CharSequence... searchStrings) {
return Strings.CS.endsWithAny(sequence, searchStrings);
}
/**
* 测试 CharSequence 是否以指定的后缀结尾。
*/
public static boolean endsWith(final CharSequence str, final CharSequence suffix) {
return Strings.CS.endsWith(str, suffix);
}
}
@@ -0,0 +1,84 @@
package org.dromara.common.core.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;
/**
* 线程工具
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ThreadUtils {
/**
* 批量执行任务
*/
public static void virtualInvokeAll(Runnable... runnableList) {
List<Future<?>> callableList = new ArrayList<>();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Runnable runnable : runnableList) {
callableList.add(executor.submit(runnable));
}
for (Future<?> future : callableList) {
future.get();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("线程执行被中断", e);
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new RuntimeException("线程执行异常:" + cause.getMessage(), cause);
}
}
/**
* 批量执行有返回值的任务
*
* @param supplierList 任务列表
* @param <T> 返回值类型
* @return 按提交顺序返回的任务结果
*/
@SafeVarargs
public static <T> List<T> virtualSubmitAll(Supplier<T>... supplierList) {
return virtualSubmitAll(List.of(supplierList));
}
/**
* 批量执行有返回值的任务
*
* @param supplierList 任务列表
* @param <T> 返回值类型
* @return 按提交顺序返回的任务结果
*/
public static <T> List<T> virtualSubmitAll(Collection<? extends Supplier<T>> supplierList) {
List<Future<T>> futureList = new ArrayList<>();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Supplier<T> supplier : supplierList) {
futureList.add(executor.submit(supplier::get));
}
List<T> resultList = new ArrayList<>(futureList.size());
for (Future<T> future : futureList) {
resultList.add(future.get());
}
return resultList;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("线程执行被中断", e);
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new RuntimeException("线程执行异常:" + cause.getMessage(), cause);
}
}
}
@@ -0,0 +1,189 @@
package org.dromara.common.core.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.lang.tree.parser.NodeParser;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 扩展 hutool TreeUtil 封装系统树构建
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TreeBuildUtils extends TreeUtil {
/**
* 根据前端定制差异化字段
*/
public static final TreeNodeConfig DEFAULT_CONFIG = TreeNodeConfig.DEFAULT_CONFIG.setNameKey("label");
/**
* 使用动态规划构建树形结构
*
* @param items 节点列表项
* @param parentId 父节点ID
* @param classifier 动态规划表分类函数
* @param action 回溯动作
* @param <K> 节点ID的类型
* @param <T> 输入节点的类型
* @return 构建好的树形结构列表
*/
public static <K, T> List<T> build(List<T> items, K parentId, Function<T, K> classifier, BiConsumer<T, Map<K, List<T>>> action) {
// 构建动态规划表 (依据父ID分组)
Map<K, List<T>> nodeTreeMaps = items.stream().collect(Collectors.groupingBy(classifier));
// 回溯构建各级节点关系
items.forEach(item -> action.accept(item, nodeTreeMaps));
return nodeTreeMaps.get(parentId);
}
/**
* 构建树形结构
*
* @param <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param list 节点列表,其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器,用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
K k = ReflectUtils.invokeGetter(list.getFirst(), "parentId");
return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
}
/**
* 构建树形结构
*
* @param <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param parentId 顶级节点
* @param list 节点列表,其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器,用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser);
}
/**
* 构建多根节点的树结构(支持多个顶级节点)
*
* @param list 原始数据列表
* @param getId 获取节点 ID 的方法引用,例如:node -> node.getId()
* @param getParentId 获取节点父级 ID 的方法引用,例如:node -> node.getParentId()
* @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点
* @param <T> 原始数据类型(如实体类、DTO 等)
* @param <K> 节点 ID 类型(如 Long、String
* @return 构建完成的树形结构(可能包含多个顶级根节点)
*/
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list, Function<T, K> getId, Function<T, K> getParentId, NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
Set<K> rootParentIds = StreamUtils.toSet(list, getParentId);
rootParentIds.removeAll(StreamUtils.toSet(list, getId));
// 构建每一个根 parentId 下的树,并合并成最终结果列表
return rootParentIds.stream()
.flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream())
.collect(Collectors.toList());
}
/**
* 获取节点列表中所有节点的叶子节点
*
* @param <K> 节点ID的类型
* @param nodes 节点列表
* @return 包含所有叶子节点的列表
*/
public static <K> List<Tree<K>> getLeafNodes(List<Tree<K>> nodes) {
if (CollUtil.isEmpty(nodes)) {
return CollUtil.newArrayList();
}
return nodes.stream()
.flatMap(TreeBuildUtils::extractLeafNodes)
.collect(Collectors.toList());
}
/**
* 构建树节点路径 Map:路径为 key,节点为 value
*
* @param trees 树结构
* @param joiner 拼接符 / - _
* @param fieldGetter 路径拼接字段(Tree::getName / Tree::getId
* @return Map<拼接路径, 原始Tree节点>
*/
public static <K> Map<String, Tree<K>> buildTreeNodeMap(List<Tree<K>> trees, String joiner, Function<Tree<K>, CharSequence> fieldGetter) {
Map<String, Tree<K>> nodeMap = new LinkedHashMap<>();
doBuildTreeNodeMap(trees, "", joiner, fieldGetter, nodeMap);
return nodeMap;
}
/**
* 获取指定节点下的所有叶子节点
*
* @param <K> 节点ID的类型
* @param node 要查找叶子节点的根节点
* @return 包含所有叶子节点的列表
*/
private static <K> Stream<Tree<K>> extractLeafNodes(Tree<K> node) {
if (!node.hasChild()) {
return Stream.of(node);
} else {
// 递归调用,获取所有子节点的叶子节点
return node.getChildren().stream()
.flatMap(TreeBuildUtils::extractLeafNodes);
}
}
/**
* 递归构建【路径为key,节点为value】的Map
* <p>深度优先遍历树结构,将拼接好的路径作为key,原始Tree节点作为value</p>
*
* @param trees 当前层级的节点列表
* @param parentPath 父节点已拼接好的路径
* @param joiner 路径拼接符,如 "/"、"-"、"_"
* @param fieldGetter 用于拼接路径的节点字段获取器
* @param nodeMap 存放最终结果的有序Map(路径->Tree节点)
* @param <K> 树节点ID的类型
*/
private static <K> void doBuildTreeNodeMap(List<Tree<K>> trees, String parentPath, String joiner, Function<Tree<K>, CharSequence> fieldGetter, Map<String, Tree<K>> nodeMap) {
if (CollUtil.isEmpty(trees)) {
return;
}
for (Tree<K> tree : trees) {
CharSequence field = fieldGetter.apply(tree);
if (StrUtil.isEmpty(field)) {
continue;
}
// 拼接路径作为 KEY
String currentPath = StrUtil.isEmpty(parentPath) ? field.toString() : parentPath + joiner + field;
// 路径 = key,节点 = value
nodeMap.put(currentPath, tree);
// 递归子节点
doBuildTreeNodeMap(tree.getChildren(), currentPath, joiner, fieldGetter, nodeMap);
}
}
}
@@ -0,0 +1,38 @@
package org.dromara.common.core.utils;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.Set;
/**
* Validator 校验框架工具
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidatorUtils {
private static final Validator VALID = SpringUtils.getBean(Validator.class);
/**
* 对给定对象进行参数校验,并根据指定的校验组进行校验
*
* @param object 要进行校验的对象
* @param groups 校验组
* @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
*/
public static <T> void validate(T object, Class<?>... groups) {
if (object == null) {
throw new RuntimeException("请求参数不能为空");
}
Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
if (!validate.isEmpty()) {
throw new ConstraintViolationException("参数校验异常", validate);
}
}
}
@@ -0,0 +1,43 @@
package org.dromara.common.core.utils.file;
import cn.hutool.core.io.FileUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 文件处理工具类
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FileUtils extends FileUtil {
/**
* 下载文件名重新编码
*
* @param response 响应对象
* @param realFileName 真实文件名
*/
public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
String percentEncodedFileName = percentEncode(realFileName);
String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
response.setHeader("Content-disposition", contentDispositionValue);
response.setHeader("download-filename", percentEncodedFileName);
}
/**
* 百分号编码工具方法
*
* @param s 需要百分号编码的字符串
* @return 百分号编码后的字符串
*/
public static String percentEncode(String s) {
String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
return encode.replaceAll("\\+", "%20");
}
}
@@ -0,0 +1,116 @@
package org.dromara.common.core.utils.file;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* 媒体类型工具类
*
* @author ruoyi
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MimeTypeUtils {
/**
* PNG 图片 MIME 类型
*/
public static final String IMAGE_PNG = "image/png";
/**
* JPG 图片 MIME 类型
*/
public static final String IMAGE_JPG = "image/jpg";
/**
* JPEG 图片 MIME 类型
*/
public static final String IMAGE_JPEG = "image/jpeg";
/**
* BMP 图片 MIME 类型
*/
public static final String IMAGE_BMP = "image/bmp";
/**
* GIF 图片 MIME 类型
*/
public static final String IMAGE_GIF = "image/gif";
/**
* 图片扩展名数组
*/
public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
/**
* Flash 文件扩展名数组
*/
public static final String[] FLASH_EXTENSION = {"swf", "flv"};
/**
* 视频文件扩展名数组
*/
public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
/**
* 媒体文件扩展名数组(音频+视频+Flash)
*/
public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", "asf", "rm", "rmvb"};
/**
* 默认允许上传的文件扩展名数组
*/
public static final String[] DEFAULT_ALLOWED_EXTENSION = {
// 图片
"bmp", "gif", "jpg", "jpeg", "png",
// word excel powerpoint
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
// 压缩文件
"rar", "zip", "gz", "bz2",
// 视频格式
"mp4", "avi", "rmvb",
// pdf
"pdf"
};
/**
* 判断文件扩展名是否为图片格式(忽略大小写)
*
* @param extension 文件扩展名
* @return 是图片返回 true,否则返回 false
*/
public static boolean isImage(String extension) {
return StrUtil.equalsAnyIgnoreCase(extension, IMAGE_EXTENSION);
}
/**
* 判断文件扩展名是否为视频格式(忽略大小写)
*
* @param extension 文件扩展名
* @return 是视频返回 true,否则返回 false
*/
public static boolean isVideo(String extension) {
return StrUtil.equalsAnyIgnoreCase(extension, VIDEO_EXTENSION);
}
/**
* 判断文件扩展名是否为媒体格式(忽略大小写)
*
* @param extension 文件扩展名
* @return 是媒体返回 true,否则返回 false
*/
public static boolean isMedia(String extension) {
return StrUtil.equalsAnyIgnoreCase(extension, MEDIA_EXTENSION);
}
/**
* 判断文件扩展名是否在默认允许上传范围内(忽略大小写)
*
* @param extension 文件扩展名
* @return 允许返回 true,不允许返回 false
*/
public static boolean isDefaultAllowed(String extension) {
return StrUtil.equalsAnyIgnoreCase(extension, DEFAULT_ALLOWED_EXTENSION);
}
}
@@ -0,0 +1,49 @@
package org.dromara.common.core.utils.ip;
import cn.hutool.http.HtmlUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.NetUtils;
import org.dromara.common.core.utils.StringUtils;
/**
* 获取地址类
*
* @author Lion Li
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AddressUtils {
// 未知IP
public static final String UNKNOWN_IP = "XX XX";
// 内网地址
public static final String LOCAL_ADDRESS = "内网IP";
/**
* 根据 IP 查询真实地址。
*
* @param ip IP 地址
* @return 真实地址
*/
public static String getRealAddressByIP(String ip) {
// 处理空串并过滤HTML标签
ip = HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(ip, ""));
// 判断是否为IPv4
boolean isIPv4 = NetUtils.isIPv4(ip);
// 判断是否为IPv6
boolean isIPv6 = NetUtils.isIPv6(ip);
// 如果不是IPv4或IPv6,则返回未知IP
if (!isIPv4 && !isIPv6) {
return UNKNOWN_IP;
}
// 内网不查询
if ((isIPv4 && NetUtils.isInnerIP(ip)) || (isIPv6 && NetUtils.isInnerIPv6(ip))) {
return LOCAL_ADDRESS;
}
// TipsIp2Region 提供了精简的IPv6地址库,精简的IPv6地址库并不能完全支持IPv6地址的查询,且准确度上可能会存在问题,如需要准确的IPv6地址查询,建议自行实现
return RegionUtils.getRegion(ip);
}
}
@@ -0,0 +1,158 @@
package org.dromara.common.core.utils.ip;
import cn.hutool.core.io.resource.ResourceUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.lionsoul.ip2region.service.Config;
import org.lionsoul.ip2region.service.Ip2Region;
import org.lionsoul.ip2region.xdb.Util;
import java.io.InputStream;
import java.time.Duration;
/**
* IP地址行政区域工具类
* 参考地址:<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">ip2region xdb java 查询客户端实现</a>
* xdb数据库文件下载:<a href="https://gitee.com/lionsoul/ip2region/tree/master/data">ip2region data</a>
*
* @author 秋辞未寒
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RegionUtils {
// 默认IPv4地址库文件路径
// 下载地址:https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v4.xdb
public static final String DEFAULT_IPV4_XDB_PATH = "ip2region_v4.xdb";
// 默认IPv6地址库文件路径
// 下载地址:https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v6.xdb
public static final String DEFAULT_IPV6_XDB_PATH = "ip2region_v6.xdb";
// 默认缓存切片大小为15MB(仅针对BufferCache全量读取有效,如果你的xdb数据库很大,合理设置该值可以有效提升BufferCache模式下的查询效率,具体可以查看Ip2Region的README
// 注意:设置过大的值可能会申请内存时,因内存不足而导致OOM,请合理设置该值。
// READMEhttps://gitee.com/lionsoul/ip2region/tree/master/binding/java
public static final int DEFAULT_CACHE_SLICE_BYTES = 1024 * 1024 * 15;
// 未知地址
public static final String UNKNOWN_ADDRESS = "未知";
// Ip2Region服务实例
private static Ip2Region ip2Region;
// 初始化Ip2Region服务实例
static {
try {
// 注意:Ip2Region 的xdb文件加载策略 CachePolicy 有三种,分别是:BufferCache(全量读取xdb到内存中)、VIndexCache(默认策略,按需读取并缓存)、NoCache(实时读取)
// 本项目工具使用的 CachePolicy 为 BufferCacheBufferCache会加载整个xdb文件到内存中,setXdbInputStream 仅支持 BufferCache 策略。
// 因为加载整个xdb文件会耗费非常大的内存,如果你不希望加载整个xdb到内存中,更推荐使用 VIndexCache 或 NoCache(即实时读取文件)策略和 setXdbPath/setXdbFile 加载方法(需要注意的一点,setXdbPath 和 setXdbFile 不支持读取ClassPath(即源码和resource目录)中的文件)。
// 一般而言,更建议把xdb数据库放到一个指定的文件目录中(即不打包进jar包中),然后使用 VIndexCache + 配合SearcherPool的并发池读取数据,更方便随时更新xdb数据库
InputStream v4InputStream = ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH);
// IPv4配置
Config v4Config = Config.custom()
.setCachePolicy(Config.BufferCache)
//.setXdbFile(v4TempXdb)
.setXdbInputStream(v4InputStream)
//
.setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES)
.asV4();
// IPv6配置
Config v6Config = null;
InputStream v6XdbInputStream = ResourceUtil.getStreamSafe(DEFAULT_IPV6_XDB_PATH);
if (v6XdbInputStream == null) {
log.warn("未加载 IPv6 地址库:未在类路径下找到文件 {}。当前仅启用 IPv4 查询。如需启用 IPv6,请将 ip2region_v6.xdb 放置到 resources 目录", DEFAULT_IPV6_XDB_PATH);
} else {
v6Config = Config.custom()
.setCachePolicy(Config.BufferCache)
//.setXdbFile(v6TempXdb)
.setXdbInputStream(v6XdbInputStream)
.setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES)
.asV6();
}
// 初始化Ip2Region实例
RegionUtils.ip2Region = Ip2Region.create(v4Config, v6Config);
log.debug("IP工具初始化成功,加载IP地址库数据成功!");
} catch (Exception e) {
throw new ServiceException("RegionUtils初始化失败,原因:{}", e.getMessage());
}
}
/**
* 根据IP地址离线获取城市
*
* @param ipString ip地址字符串
*/
public static String getRegion(String ipString) {
try {
String region = ip2Region.search(ipString);
if (StringUtils.isBlank(region)) {
return UNKNOWN_ADDRESS;
}
return StringUtils.replace(region, "0", UNKNOWN_ADDRESS);
} catch (Exception e) {
log.error("IP地址离线获取城市异常 {}", ipString);
return UNKNOWN_ADDRESS;
}
}
/**
* 根据IP地址离线获取城市
*
* @param ipBytes ip地址字节数组
*/
public static String getRegion(byte[] ipBytes) {
try {
String region = ip2Region.search(ipBytes);
if (StringUtils.isBlank(region)) {
return UNKNOWN_ADDRESS;
}
return StringUtils.replace(region, "0", UNKNOWN_ADDRESS);
} catch (Exception e) {
log.error("IP地址离线获取城市异常 {}", Util.ipToString(ipBytes));
return UNKNOWN_ADDRESS;
}
}
/**
* 关闭Ip2Region服务
*/
public static void close() {
if (ip2Region == null) {
return;
}
try {
ip2Region.close(10000);
} catch (Exception e) {
log.error("Ip2Region服务关闭异常", e);
}
}
/**
* 关闭Ip2Region服务
*
* @param timeout 关闭超时时间
*/
public static void close(final Duration timeout) {
if (ip2Region == null) {
return;
}
if (timeout == null) {
close();
return;
}
try {
ip2Region.close(timeout.toMillis());
} catch (Exception e) {
log.error("Ip2Region服务关闭异常", e);
}
}
}
@@ -0,0 +1,63 @@
package org.dromara.common.core.utils.reflect;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.lang.Dict;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
/**
* 注解工具类
*
* @author 秋辞未寒
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AnnotationUtils extends AnnotationUtil {
/**
* 获取指定注解
*
* @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission
* @param annotationTypeName 注解类型名称
* @return 注解对象
*/
@SuppressWarnings("unchecked")
public static Annotation getAnnotation(AnnotatedElement annotationEle, String annotationTypeName) {
try {
return AnnotationUtil.getAnnotation(annotationEle, (Class<? extends Annotation>) Class.forName(annotationTypeName));
} catch (final ClassNotFoundException | ClassCastException e) {
// ignore
log.error("AnnotationUtils.getAnnotation(AnnotatedElement, String) error.", e);
return null;
}
}
/**
* 获取指定注解中所有属性值<br>
* 如果无指定的属性方法返回null
*
* @param annotationEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission
* @param annotationTypeName 注解类型名称
* @return 注解对象所有属性键值
* @throws UtilException 调用注解中的方法时执行异常
*/
@SuppressWarnings("unchecked")
public static Dict getAnnotationValueMap(AnnotatedElement annotationEle, String annotationTypeName) throws UtilException {
try {
Map<String, Object> annotationValueMap = AnnotationUtil.getAnnotationValueMap(annotationEle, (Class<? extends Annotation>) Class.forName(annotationTypeName));
return new Dict(annotationValueMap);
} catch (final ClassNotFoundException | ClassCastException e) {
// ignore
log.error("AnnotationUtils.getAnnotationValueMap(AnnotatedElement, String) error.", e);
return null;
}
}
}
@@ -0,0 +1,55 @@
package org.dromara.common.core.utils.reflect;
import cn.hutool.core.util.ReflectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import java.lang.reflect.Method;
/**
* 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ReflectUtils extends ReflectUtil {
private static final String SETTER_PREFIX = "set";
private static final String GETTER_PREFIX = "get";
/**
* 调用Getter方法.
* 支持多级,如:对象名.对象名.方法
*/
@SuppressWarnings("unchecked")
public static <E> E invokeGetter(Object obj, String propertyName) {
Object object = obj;
for (String name : StringUtils.split(propertyName, ".")) {
String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
object = invoke(object, getterMethodName);
}
return (E) object;
}
/**
* 调用Setter方法, 仅匹配方法名。
* 支持多级,如:对象名.对象名.方法
*/
public static <E> void invokeSetter(Object obj, String propertyName, E value) {
Object object = obj;
String[] names = StringUtils.split(propertyName, ".");
for (int i = 0; i < names.length; i++) {
if (i < names.length - 1) {
String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
object = invoke(object, getterMethodName);
} else {
String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
Method method = getMethodByName(object.getClass(), setterMethodName);
invoke(object, method, value);
}
}
}
}
@@ -0,0 +1,34 @@
package org.dromara.common.core.utils.regex;
import cn.hutool.core.util.ReUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.constant.RegexConstants;
/**
* 正则相关工具类
*
* @author AprilWind
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class RegexUtils extends ReUtil {
/**
* 从输入字符串中提取匹配的部分,如果没有匹配则返回默认值
*
* @param input 要提取的输入字符串
* @param regex 用于匹配的正则表达式,可以使用 {@link RegexConstants} 中定义的常量
* @param defaultInput 如果没有匹配时返回的默认值
* @return 如果找到匹配的部分,则返回匹配的部分,否则返回默认值
*/
public static String extractFromString(String input, String regex, String defaultInput) {
try {
String str = ReUtil.get(regex, input, 1);
return str == null ? defaultInput : str;
} catch (Exception e) {
return defaultInput;
}
}
}
@@ -0,0 +1,108 @@
package org.dromara.common.core.utils.regex;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.core.lang.Validator;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.factory.RegexPatternPoolFactory;
import java.util.regex.Pattern;
/**
* 正则字段校验器
* 主要验证字段非空、是否为满足指定格式等
*
* @author AprilWind
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RegexValidator extends Validator {
/**
* 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
*/
public static final Pattern DICTIONARY_TYPE = RegexPatternPoolFactory.DICTIONARY_TYPE;
/**
* 身份证号码(后6位)
*/
public static final Pattern ID_CARD_LAST_6 = RegexPatternPoolFactory.ID_CARD_LAST_6;
/**
* QQ号码
*/
public static final Pattern QQ_NUMBER = RegexPatternPoolFactory.QQ_NUMBER;
/**
* 邮政编码
*/
public static final Pattern POSTAL_CODE = RegexPatternPoolFactory.POSTAL_CODE;
/**
* 注册账号
*/
public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
/**
* 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
*/
public static final Pattern PASSWORD = RegexPatternPoolFactory.PASSWORD;
/**
* 通用状态(0表示正常,1表示停用)
*/
public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
/**
* 检查输入的账号是否匹配预定义的规则
*
* @param value 要验证的账号
* @return 如果账号符合规则,返回 true;否则,返回 false。
*/
public static boolean isAccount(CharSequence value) {
return isMatchRegex(ACCOUNT, value);
}
/**
* 验证输入的账号是否符合规则,如果不符合,则抛出 ValidateException 异常
*
* @param value 要验证的账号
* @param errorMsg 验证失败时抛出的异常消息
* @param <T> CharSequence 的子类型
* @return 如果验证通过,返回输入的账号
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> T validateAccount(T value, String errorMsg) throws ValidateException {
if (!isAccount(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
/**
* 检查输入的状态是否匹配预定义的规则
*
* @param value 要验证的状态
* @return 如果状态符合规则,返回 true;否则,返回 false。
*/
public static boolean isStatus(CharSequence value) {
return isMatchRegex(STATUS, value);
}
/**
* 验证输入的状态是否符合规则,如果不符合,则抛出 ValidateException 异常
*
* @param value 要验证的状态
* @param errorMsg 验证失败时抛出的异常消息
* @param <T> CharSequence 的子类型
* @return 如果验证通过,返回输入的状态
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> T validateStatus(T value, String errorMsg) throws ValidateException {
if (!isStatus(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
}
@@ -0,0 +1,67 @@
package org.dromara.common.core.utils.sql;
import cn.hutool.core.exceptions.UtilException;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
/**
* sql操作工具类
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SqlUtil {
/**
* 定义常用的 sql关键字
*/
public static final String SQL_REGEX = "\u000B|%0A|and |extractvalue|updatexml|sleep|information_schema|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
/**
* 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
*/
public static final String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
/**
* 检查字符,防止注入绕过
*/
public static String escapeOrderBySql(String value) {
if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
throw new IllegalArgumentException("参数不符合规范,不能进行查询");
}
return value;
}
/**
* 验证 order by 语法是否符合规范
*/
public static boolean isValidOrderBySql(String value) {
return value.matches(SQL_PATTERN);
}
/**
* SQL关键字检查
*/
public static void filterKeyword(String value) {
if (StringUtils.isEmpty(value)) {
return;
}
// ==================== 核心增强:自动转义单引号 ====================
// 不抛异常、不破坏业务、不改变原方法行为、自动防注入
if (value.contains("'")) {
throw new UtilException("请求参数包含非法字符【'】,已禁止执行");
}
// ==================== 原有逻辑不变 ====================
String normalizedValue = value.replaceAll("\\p{Z}|\\s", "");
String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
for (String sqlKeyword : sqlKeywords) {
if (StringUtils.indexOf(normalizedValue, sqlKeyword) > -1) {
throw new UtilException("请求参数包含敏感关键词'" + sqlKeyword + "',可能存在安全风险");
}
}
}
}
@@ -0,0 +1,9 @@
package org.dromara.common.core.validate;
/**
* 校验分组 add
*
* @author Lion Li
*/
public interface AddGroup {
}
@@ -0,0 +1,9 @@
package org.dromara.common.core.validate;
/**
* 校验分组 edit
*
* @author Lion Li
*/
public interface EditGroup {
}
@@ -0,0 +1,9 @@
package org.dromara.common.core.validate;
/**
* 校验分组 query
*
* @author Lion Li
*/
public interface QueryGroup {
}
@@ -0,0 +1,46 @@
package org.dromara.common.core.validate.dicts;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 字典项校验注解
*
* @author AprilWind
*/
@Constraint(validatedBy = DictPatternValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface DictPattern {
/**
* 字典类型,如 "sys_user_gender"
*/
String dictType();
/**
* 分隔符
*/
String separator() default ",";
/**
* 默认校验失败提示信息
*/
String message() default "字典值无效";
/**
* Bean Validation 分组。
*/
Class<?>[] groups() default {};
/**
* Bean Validation 负载信息。
*/
Class<? extends Payload>[] payload() default {};
}
@@ -0,0 +1,58 @@
package org.dromara.common.core.validate.dicts;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
/**
* 自定义字典值校验器
*
* @author AprilWind
*/
public class DictPatternValidator implements ConstraintValidator<DictPattern, String> {
/**
* 字典类型
*/
private String dictType;
/**
* 分隔符
*/
private String separator = ",";
/**
* 初始化校验器,提取注解上的字典类型
*
* @param annotation 注解实例
*/
@Override
public void initialize(DictPattern annotation) {
this.dictType = annotation.dictType();
if (StringUtils.isNotBlank(annotation.separator())) {
this.separator = annotation.separator();
}
}
/**
* 校验字段值是否为指定字典类型中的合法值
*
* @param value 被校验的字段值
* @param context 校验上下文(可用于构建错误信息)
* @return true 表示校验通过(合法字典值),false 表示不通过
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isBlank(value)) {
return true;
}
if (StringUtils.isBlank(dictType)) {
return false;
}
String dictLabel = SpringUtils.getBean(DictService.class).getDictLabel(dictType, value, separator);
return StringUtils.isNotBlank(dictLabel);
}
}
@@ -0,0 +1,67 @@
package org.dromara.common.core.validate.enums;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义枚举校验
*
* @author 秋辞未寒
* @date 2024-12-09
*/
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(EnumPattern.List.class) // 允许在同一元素上多次使用该注解
@Constraint(validatedBy = {EnumPatternValidator.class})
public @interface EnumPattern {
/**
* 需要校验的枚举类型
*/
Class<? extends Enum<?>> type();
/**
* 枚举类型校验值字段名称
* 需确保该字段实现了 getter 方法
*/
String fieldName();
/**
* 校验失败时返回的错误消息。
*/
String message() default "输入值不在枚举范围内";
/**
* Bean Validation 分组。
*/
Class<?>[] groups() default {};
/**
* Bean Validation 负载信息。
*/
Class<? extends Payload>[] payload() default {};
/**
* 支持在同一元素上重复声明多个枚举校验规则。
*/
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@interface List {
/**
* 重复声明的枚举校验规则集合。
*/
EnumPattern[] value();
}
}
@@ -0,0 +1,59 @@
package org.dromara.common.core.validate.enums;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.HashSet;
import java.util.Set;
/**
* 自定义枚举校验注解实现
*
* @author 秋辞未寒
* @date 2024-12-09
*/
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
/**
* 枚举允许值集合。
*/
private final Set<String> values = new HashSet<>();
/**
* 初始化枚举允许值集合。
*
* @param annotation 枚举校验注解
*/
@Override
public void initialize(EnumPattern annotation) {
ConstraintValidator.super.initialize(annotation);
String fieldName = annotation.fieldName();
if (StringUtils.isBlank(fieldName)) {
return;
}
for (Object e : annotation.type().getEnumConstants()) {
Object fieldValue = ReflectUtils.invokeGetter(e, fieldName);
if (fieldValue != null) {
values.add(String.valueOf(fieldValue));
}
}
}
/**
* 校验字符串是否在枚举允许值集合内。
*
* @param value 待校验值
* @param constraintValidatorContext 校验上下文
* @return true 校验通过 false 校验失败
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isBlank(value)) {
return true;
}
return values.contains(value);
}
}
@@ -0,0 +1,36 @@
package org.dromara.common.core.xss;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义xss校验注解
*
* @author Lion Li
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Constraint(validatedBy = {XssValidator.class})
public @interface Xss {
/**
* 校验失败时返回的错误消息。
*/
String message() default "不允许任何脚本运行";
/**
* Bean Validation 分组。
*/
Class<?>[] groups() default {};
/**
* Bean Validation 负载信息。
*/
Class<? extends Payload>[] payload() default {};
}
@@ -0,0 +1,31 @@
package org.dromara.common.core.xss;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.HtmlUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.dromara.common.core.utils.StringUtils;
/**
* 自定义xss校验注解实现
*
* @author Lion Li
*/
public class XssValidator implements ConstraintValidator<Xss, String> {
/**
* 校验字符串是否包含 HTML 标签。
*
* @param value 待校验值
* @param constraintValidatorContext 校验上下文
* @return true 校验通过 false 包含 HTML 标签
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isBlank(value)) {
return true;
}
return !ReUtil.contains(HtmlUtil.RE_HTML_MARK, value);
}
}
@@ -0,0 +1,4 @@
org.dromara.common.core.config.ApplicationConfig
org.dromara.common.core.config.ThreadPoolConfig
org.dromara.common.core.config.ValidatorConfig
org.dromara.common.core.utils.SpringUtils
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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-doc</artifactId>
<description>
ruoyi-common-doc 系统接口
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- Web 服务器抽象 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-web-server</artifactId>
</dependency>
<!-- 接口文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
</dependency>
<!-- JavaDoc 运行时读取 -->
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
</dependency>
<!-- Jackson Kotlin 模块 -->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,154 @@
package org.dromara.common.doc.config;
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.core.customizer.ClassTagOperationCustomizer;
import org.dromara.common.doc.core.customizer.JavadocOperationCustomizer;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.context.annotation.Bean;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* 接口文档配置
*
* @author Lion Li
*/
@RequiredArgsConstructor
@AutoConfiguration(before = SpringDocConfiguration.class)
@EnableConfigurationProperties(SpringDocProperties.class)
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
public class SpringDocConfig {
private final ServerProperties serverProperties;
/**
* 构建基础 OpenAPI 文档对象。
*
* @param properties SpringDoc 配置
* @return OpenAPI 对象
*/
@Bean
@ConditionalOnMissingBean(OpenAPI.class)
public OpenAPI openApi(SpringDocProperties properties) {
OpenAPI openApi = new OpenAPI();
// 文档基本信息
SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
Info info = convertInfo(infoProperties);
openApi.info(info);
// 扩展文档信息
openApi.externalDocs(properties.getExternalDocs());
openApi.tags(properties.getTags());
openApi.paths(properties.getPaths());
if (properties.getComponents() != null) {
openApi.components(properties.getComponents());
Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
List<SecurityRequirement> list = new ArrayList<>();
SecurityRequirement securityRequirement = new SecurityRequirement();
keySet.forEach(securityRequirement::addList);
list.add(securityRequirement);
openApi.security(list);
}
return openApi;
}
/**
* 将自定义文档信息配置转换为 OpenAPI Info。
*
* @param infoProperties 文档信息配置
* @return Info 对象
*/
private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
Info info = new Info();
info.setTitle(infoProperties.getTitle());
info.setDescription(infoProperties.getDescription());
info.setContact(infoProperties.getContact());
info.setLicense(infoProperties.getLicense());
info.setVersion(infoProperties.getVersion());
return info;
}
/**
* Controller 类级标签增强
*/
@Bean
public ClassTagOperationCustomizer classTagOperationCustomizer(Optional<JavadocProvider> javadocProvider,
PropertyResolverUtils propertyResolverUtils) {
return new ClassTagOperationCustomizer(javadocProvider, propertyResolverUtils);
}
/**
* 方法 JavaDoc 与权限描述增强
*/
@Bean
public JavadocOperationCustomizer javadocOperationCustomizer(Optional<JavadocProvider> javadocProvider,
List<JavadocResolver> javadocResolvers) {
return new JavadocOperationCustomizer(javadocProvider, javadocResolvers);
}
/**
* 对已经生成好的 OpenApi 进行自定义操作
*/
@Bean
public GlobalOpenApiCustomizer openApiCustomizer() {
String contextPath = serverProperties.getServlet().getContextPath();
String finalContextPath;
if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
finalContextPath = "";
} else {
finalContextPath = contextPath;
}
// 对所有路径增加前置上下文路径
return openApi -> {
Paths oldPaths = openApi.getPaths();
if (oldPaths instanceof PlusPaths) {
return;
}
PlusPaths newPaths = new PlusPaths();
oldPaths.forEach((k, v) -> newPaths.addPathItem(finalContextPath + k, v));
openApi.setPaths(newPaths);
};
}
/**
* 注册SaToken JavaDoc权限注解解析器
*/
@Bean
public JavadocResolver saTokenAnnotationJavadocResolver() {
return new SaTokenAnnotationMetadataJavadocResolver();
}
/**
* 单独使用一个类便于判断 解决springdoc路径拼接重复问题
*
* @author Lion Li
*/
static class PlusPaths extends Paths {
/**
* 构造路径缓存标记对象。
*/
public PlusPaths() {
super();
}
}
}
@@ -0,0 +1,94 @@
package org.dromara.common.doc.config.properties;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import java.util.List;
/**
* swagger 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "springdoc")
public class SpringDocProperties {
/**
* 文档基本信息
*/
@NestedConfigurationProperty
private InfoProperties info = new InfoProperties();
/**
* 扩展文档地址
*/
@NestedConfigurationProperty
private ExternalDocumentation externalDocs;
/**
* 标签
*/
private List<Tag> tags = null;
/**
* 路径
*/
@NestedConfigurationProperty
private Paths paths = null;
/**
* 组件
*/
@NestedConfigurationProperty
private Components components = null;
/**
* <p>
* 文档的基础属性信息
* </p>
*
* @see io.swagger.v3.oas.models.info.Info
* <p>
* 为了 springboot 自动生产配置提示信息,所以这里复制一个类出来
*/
@Data
public static class InfoProperties {
/**
* 标题
*/
private String title = null;
/**
* 描述
*/
private String description = null;
/**
* 联系人信息
*/
@NestedConfigurationProperty
private Contact contact = null;
/**
* 许可证
*/
@NestedConfigurationProperty
private License license = null;
/**
* 版本
*/
private String version = null;
}
}
@@ -0,0 +1,203 @@
package org.dromara.common.doc.core.customizer;
import cn.hutool.core.io.IoUtil;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.annotations.tags.Tags;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
import java.io.StringReader;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Controller 类级标签增强。
*
* @author Lion Li
*/
@RequiredArgsConstructor
public class ClassTagOperationCustomizer implements GlobalOperationCustomizer, GlobalOpenApiCustomizer {
/**
* JavaDoc 提供器。
*/
private final Optional<JavadocProvider> javadocProvider;
/**
* SpringDoc 属性解析工具。
*/
private final PropertyResolverUtils propertyResolverUtils;
/**
* 已解析的 OpenAPI 顶层标签缓存。
*/
private final Map<String, Tag> tags = new ConcurrentHashMap<>();
/**
* 已被类 JavaDoc 替换的自动标签名称集合。
*/
private final Set<String> replacedAutoTagNames = ConcurrentHashMap.newKeySet();
/**
* 自定义接口操作的标签信息。
*
* @param operation OpenAPI 操作对象
* @param handlerMethod 处理器方法
* @return 自定义后的 OpenAPI 操作对象
*/
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
List<io.swagger.v3.oas.annotations.tags.Tag> classTags = getClassTags(beanType);
if (!CollectionUtils.isEmpty(classTags)) {
// 优先使用 Controller 类上的 @Tag / @Tags,保持 Swagger 原生注解语义
addAnnotationTags(operation, classTags);
return operation;
}
String tagName = getClassJavadocTagName(beanType);
if (StringUtils.isBlank(tagName)) {
return operation;
}
String autoTagName = OpenAPIService.splitCamelCase(beanType.getSimpleName());
if (!shouldUseClassJavadocTag(operation, autoTagName)) {
return operation;
}
// 无显式 @Tag 时,将 springdoc 自动生成的类名 tag 替换为类 JavaDoc 第一行
removeOperationTag(operation, autoTagName);
addOperationTag(operation, tagName);
replacedAutoTagNames.add(autoTagName);
tags.putIfAbsent(tagName, new Tag().name(tagName).description(javadocProvider.get().getClassJavadoc(beanType)));
return operation;
}
/**
* 自定义 OpenAPI 顶层标签集合。
*
* @param openApi OpenAPI 文档对象
*/
@Override
public void customise(OpenAPI openApi) {
if (!CollectionUtils.isEmpty(openApi.getTags()) && !CollectionUtils.isEmpty(replacedAutoTagNames)) {
// 移除已被 JavaDoc tag 替换的默认类名 tag,避免 Swagger UI 出现空分组
openApi.getTags().removeIf(tag -> replacedAutoTagNames.contains(tag.getName()));
}
// 将类级 @Tag 描述或 JavaDoc 描述补充到 OpenAPI 顶层 tags
tags.values().forEach(tag -> {
if (openApi.getTags() == null || openApi.getTags().stream().noneMatch(item -> Objects.equals(item.getName(), tag.getName()))) {
openApi.addTagsItem(tag);
}
});
}
/**
* 获取 Controller 类上的 Swagger 标签注解。
*
* @param beanType Controller 类型
* @return 标签注解列表
*/
private List<io.swagger.v3.oas.annotations.tags.Tag> getClassTags(Class<?> beanType) {
Set<Tags> tagsSet = AnnotatedElementUtils.findAllMergedAnnotations(beanType, Tags.class);
Set<io.swagger.v3.oas.annotations.tags.Tag> mergedTags = tagsSet.stream()
.flatMap(item -> Stream.of(item.value()))
.collect(Collectors.toSet());
mergedTags.addAll(AnnotatedElementUtils.findAllMergedAnnotations(beanType, io.swagger.v3.oas.annotations.tags.Tag.class));
return new ArrayList<>(mergedTags);
}
/**
* 将类级 Swagger 标签追加到接口操作与顶层标签缓存。
*
* @param operation OpenAPI 操作对象
* @param classTags 类级标签注解
*/
private void addAnnotationTags(Operation operation, List<io.swagger.v3.oas.annotations.tags.Tag> classTags) {
classTags.stream()
.map(io.swagger.v3.oas.annotations.tags.Tag::name)
.map(name -> propertyResolverUtils.resolve(name, Locale.getDefault()))
.filter(StringUtils::isNotBlank)
.forEach(name -> addOperationTag(operation, name));
AnnotationsUtils.getTags(classTags.toArray(new io.swagger.v3.oas.annotations.tags.Tag[0]), true)
.ifPresent(items -> items.forEach(tag -> {
tag.name(propertyResolverUtils.resolve(tag.getName(), Locale.getDefault()));
tag.description(propertyResolverUtils.resolve(tag.getDescription(), Locale.getDefault()));
if (StringUtils.isNotBlank(tag.getName())) {
tags.putIfAbsent(tag.getName(), tag);
}
}));
}
/**
* 获取类 JavaDoc 第一行作为标签名称。
*
* @param beanType Controller 类型
* @return 标签名称,未配置 JavaDoc 时返回 null
*/
private String getClassJavadocTagName(Class<?> beanType) {
if (javadocProvider.isEmpty()) {
return null;
}
String description = javadocProvider.get().getClassJavadoc(beanType);
if (StringUtils.isBlank(description)) {
return null;
}
// 与原 OpenApiHandler 保持一致:类 JavaDoc 第一行作为 tag 名,完整 JavaDoc 作为 tag 描述
List<String> lines = IoUtil.readLines(new StringReader(description), new ArrayList<>());
return lines.stream().filter(StringUtils::isNotBlank).findFirst().orElse(null);
}
/**
* 判断当前操作是否可以使用类 JavaDoc 标签替换默认标签。
*
* @param operation OpenAPI 操作对象
* @param autoTagName SpringDoc 自动生成的标签名
* @return true 可以替换 false 不替换
*/
private boolean shouldUseClassJavadocTag(Operation operation, String autoTagName) {
return CollectionUtils.isEmpty(operation.getTags()) || operation.getTags().contains(autoTagName);
}
/**
* 为接口操作追加标签。
*
* @param operation OpenAPI 操作对象
* @param tagName 标签名称
*/
private void addOperationTag(Operation operation, String tagName) {
if (operation.getTags() == null) {
operation.setTags(new ArrayList<>());
}
if (!operation.getTags().contains(tagName)) {
operation.addTagsItem(tagName);
}
}
/**
* 从接口操作中移除指定标签。
*
* @param operation OpenAPI 操作对象
* @param tagName 标签名称
*/
private void removeOperationTag(Operation operation, String tagName) {
if (!CollectionUtils.isEmpty(operation.getTags())) {
operation.getTags().removeIf(item -> Objects.equals(item, tagName));
}
}
}
@@ -0,0 +1,71 @@
package org.dromara.common.doc.core.customizer;
import io.swagger.v3.oas.models.Operation;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
import java.util.List;
import java.util.Optional;
/**
* 方法 JavaDoc 与扩展描述增强。
*
* @author Lion Li
*/
@RequiredArgsConstructor
public class JavadocOperationCustomizer implements GlobalOperationCustomizer {
/**
* JavaDoc 提供器。
*/
private final Optional<JavadocProvider> javadocProvider;
/**
* JavaDoc 扩展解析器列表。
*/
private final List<JavadocResolver> javadocResolvers;
/**
* 自定义接口操作的 JavaDoc 描述。
*
* @param operation OpenAPI 操作对象
* @param handlerMethod 处理器方法
* @return 自定义后的 OpenAPI 操作对象
*/
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
javadocProvider.ifPresent(provider -> {
String description = provider.getMethodJavadocDescription(handlerMethod.getMethod());
if (StringUtils.isNotBlank(description)) {
// 使用方法 JavaDoc 首句作为接口摘要,完整 JavaDoc 作为接口描述
operation.setSummary(provider.getFirstSentence(description));
operation.setDescription(description);
}
});
if (CollectionUtils.isEmpty(javadocResolvers)) {
return operation;
}
StringBuilder description = new StringBuilder(Optional.ofNullable(operation.getDescription()).orElse(""));
List<String> resolvedDescriptions = javadocResolvers.stream()
.sorted()
// 只执行支持当前 HandlerMethod 的扩展解析器,避免无注解接口被追加权限说明
.filter(resolver -> resolver.supports(handlerMethod))
.map(resolver -> resolver.resolve(handlerMethod, operation))
.filter(StringUtils::isNotBlank)
.toList();
if (!resolvedDescriptions.isEmpty()) {
// 在原方法 JavaDoc 后追加权限等扩展描述,保持现有 resolver 扩展点
resolvedDescriptions.forEach(description::append);
operation.setDescription(description.toString());
}
return operation;
}
}
@@ -0,0 +1,176 @@
package org.dromara.common.doc.core.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 存储权限框架注解解析后的权限和角色信息
*
* @author AprilWind
*/
@Data
@JsonInclude(Include.NON_EMPTY)
public class SaTokenSecurityMetadata {
/**
* 权限校验信息列表(对应 @SaCheckPermission 注解)
*/
private List<AuthInfo> permissions = new ArrayList<>();
/**
* 角色校验信息列表(对应 @SaCheckRole 注解)
*/
private List<AuthInfo> roles = new ArrayList<>();
/**
* 是否忽略校验(对应 @SaIgnore 注解)
*/
private boolean ignore = false;
/**
* 添加权限信息
*
* @param values 权限值数组
* @param mode 校验模式(AND/OR
* @param type 权限类型
* @param orRoles 或角色数组
*/
public void addPermission(String[] values, String mode, String type, String[] orRoles) {
if (values != null && values.length > 0) {
AuthInfo authInfo = new AuthInfo();
authInfo.setValues(values);
authInfo.setMode(mode);
authInfo.setType(type);
if (orRoles != null && orRoles.length > 0) {
authInfo.setOrValues(orRoles);
authInfo.setOrType("role");
}
this.permissions.add(authInfo);
}
}
/**
* 添加角色信息
*
* @param values 角色值数组
* @param mode 校验模式(AND/OR
* @param type 角色类型
*/
public void addRole(String[] values, String mode, String type) {
if (values != null && values.length > 0) {
AuthInfo authInfo = new AuthInfo();
authInfo.setValues(values);
authInfo.setMode(mode);
authInfo.setType(type);
this.roles.add(authInfo);
}
}
/**
* 生成 Markdown 结构的权限说明
*
* @return Markdown 文本
*/
public String toMarkdownString() {
StringBuilder sb = new StringBuilder();
sb.append("<br><h3>访问权限</h3><br>");
if (ignore) {
sb.append("> **权限策略**:忽略权限检查<br>");
return sb.toString();
}
if (!ignore && permissions.isEmpty() && roles.isEmpty()) {
sb.append("> **权限策略**:需要登录<br><br>");
return sb.toString();
}
if (!permissions.isEmpty()) {
sb.append("**权限校验:**<br><br>");
permissions.forEach(p -> {
String permTags = Arrays.stream(p.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(p.getModeSymbol()));
sb.append("- ").append(permTags).append("<br>");
if (p.getOrValues() != null && p.getOrValues().length > 0) {
String orTags = Arrays.stream(p.getOrValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(p.getModeSymbol()));
sb.append(" - 或角色:").append(orTags).append("<br>");
}
});
sb.append("<br>");
}
if (!roles.isEmpty()) {
sb.append("**角色校验:**<br><br>");
roles.forEach(r -> {
String roleTags = Arrays.stream(r.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(r.getModeSymbol()));
sb.append("- ").append(roleTags).append("<br>");
});
}
return sb.toString().trim();
}
/**
* 认证信息
*/
@Data
@JsonInclude(Include.NON_EMPTY)
public static class AuthInfo {
/**
* 权限或角色值数组
*/
private String[] values;
/**
* 校验模式(AND/OR
*/
private String mode;
/**
* 类型说明
*/
private String type;
/**
* 或权限/角色值数组(用于权限校验时的或角色校验)
*/
private String[] orValues;
/**
* 或值的类型(role/permission
*/
private String orType;
/**
* 重写mode的获取方法,返回符号而非文字
*
* @return AND→&OR→|,默认→&
*/
public String getModeSymbol() {
if (mode == null) {
return " & "; // 默认AND,返回&
}
return "AND".equalsIgnoreCase(mode) ? " & " : " | ";
}
}
}
@@ -0,0 +1,217 @@
package org.dromara.common.doc.core.resolver;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.ClassLoaderUtil;
import io.swagger.v3.oas.models.Operation;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
import java.util.function.Supplier;
/**
* 抽象元数据 Javadoc 解析器
*
* @param <M> 元数据类型
* @author 秋辞未寒
*/
public abstract class AbstractMetadataJavadocResolver<M> implements JavadocResolver {
/**
* 最高优先级。
*/
public static final int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
/**
* 最低优先级。
*/
public static final int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
/**
* 元数据提供者。
*/
private final Supplier<M> metadataProvider;
/**
* 解析器排序值。
*/
private final int order;
/**
* 构造元数据 Javadoc 解析器。
*
* @param metadataProvider 元数据提供者
*/
public AbstractMetadataJavadocResolver(Supplier<M> metadataProvider) {
this(metadataProvider, LOWEST_PRECEDENCE);
}
/**
* 构造带排序值的元数据 Javadoc 解析器。
*
* @param metadataProvider 元数据提供者
* @param order 排序值
*/
public AbstractMetadataJavadocResolver(Supplier<M> metadataProvider, int order) {
this.metadataProvider = metadataProvider;
this.order = order;
}
/**
* 获取解析器排序值。
*
* @return 排序值
*/
@Override
public int getOrder() {
return order;
}
/**
* 使用当前元数据解析接口文档描述。
*
* @param handlerMethod 处理器方法
* @param operation Swagger Operation 实例
* @return 解析后的 Javadoc 内容
*/
@Override
public String resolve(HandlerMethod handlerMethod, Operation operation) {
return resolve(handlerMethod, operation, metadataProvider.get());
}
/**
* 执行解析并返回解析到的 Javadoc 内容
*
* @param handlerMethod 处理器方法
* @param operation Swagger Operation实例
* @param metadata 元信息
* @return 解析到的 Javadoc 内容
*/
public abstract String resolve(HandlerMethod handlerMethod, Operation operation, M metadata);
/**
* 检查处理器方法所属的类上是否存在注解
*
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasClassAnnotation(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationClass);
}
/**
* 检查处理器方法所属的类上是否存在注解
*
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasClassAnnotation(HandlerMethod handlerMethod, String annotationTypeName) {
return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationTypeName);
}
/**
* 检查处理器方法上是否存在注解
*
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasMethodAnnotation(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationClass);
}
/**
* 检查处理器方法上是否存在注解
*
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasMethodAnnotation(HandlerMethod handlerMethod, String annotationTypeName) {
return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationTypeName);
}
/**
* 检查处理器方法上是否存在注解
*
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasAnnotation(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return this.hasClassAnnotation(handlerMethod, annotationClass) || this.hasMethodAnnotation(handlerMethod, annotationClass);
}
/**
* 检查处理器方法上是否存在注解
*
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasAnnotation(HandlerMethod handlerMethod, String annotationTypeName) {
return this.hasClassAnnotation(handlerMethod, annotationTypeName) || this.hasMethodAnnotation(handlerMethod, annotationTypeName);
}
/**
* 获取处理器方法所属类上的注解的值
*
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 注解的值
*/
public Map<String, Object> getClassAnnotationValueMap(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass);
}
/**
* 获取处理器方法所属类上的注解的值
*
* @param handlerMethod 处理器方法
* @param annotationClassName 注解类名称
* @return 注解的值
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getClassAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) {
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(annotationClassName, false);
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass);
}
/**
* 获取处理器方法上的注解的值
*
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 注解的值
*/
public Map<String, Object> getMethodAnnotationValueMap(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass);
}
/**
* 获取处理器方法所属类上的注解的值
*
* @param handlerMethod 处理器方法
* @param annotationClassName 注解类名称
* @return 注解的值
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getMethodAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) {
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(annotationClassName, false);
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass);
}
/**
* 获取指定元素上的注解属性映射。
*
* @param annotatedElement 被注解元素
* @param annotationClass 注解类型
* @return 注解属性映射
*/
private Map<String, Object> getAnnotationValueMap(AnnotatedElement annotatedElement, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(annotatedElement, annotationClass);
}
}
@@ -0,0 +1,61 @@
package org.dromara.common.doc.core.resolver;
import io.swagger.v3.oas.models.Operation;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
/**
* Javadoc解析器接口
*
* @author echo
* @author 秋辞未寒
*/
public interface JavadocResolver extends Comparable<JavadocResolver>, Ordered {
/**
* 检查解析器是否支持解析 HandlerMethod
*
* @param handlerMethod 处理器方法
* @return 是否支持解析
*/
boolean supports(HandlerMethod handlerMethod);
/**
* 执行解析并返回解析到的 Javadoc 内容
*
* @param handlerMethod 处理器方法
* @param operation Swagger Operation实例
* @return 解析到的 Javadoc 内容
*/
String resolve(HandlerMethod handlerMethod, Operation operation);
/**
* 获取解析器优先级。
*
* @return 优先级
*/
default int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
/**
* 获取解析器的名称
*
* @return 解析器名称
*/
default String getName() {
return this.getClass().getSimpleName();
}
/**
* 比较解析器执行顺序。
*
* @param o 其他解析器
* @return 比较结果
*/
@Override
default int compareTo(@NotNull JavadocResolver o) {
return Integer.compare(getOrder(), o.getOrder());
}
}
@@ -0,0 +1,231 @@
package org.dromara.common.doc.core.resolver;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassLoaderUtil;
import io.swagger.v3.oas.models.Operation;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/**
* 基于JavaDoc的SaToken权限解析器
*
* @author echo
* @author 秋辞未寒
*/
@SuppressWarnings("unchecked")
@Slf4j
public class SaTokenAnnotationMetadataJavadocResolver extends AbstractMetadataJavadocResolver<SaTokenSecurityMetadata> {
/**
* 默认元数据提供者,每次解析都会创建一个新的元数据对象
*/
public static final Supplier<SaTokenSecurityMetadata> DEFAULT_METADATA_PROVIDER = SaTokenSecurityMetadata::new;
/**
* Sa-Token 注解包名。
*/
private static final String BASE_CLASS_NAME = "cn.dev33.satoken.annotation";
/**
* Sa-Token 角色校验注解类名。
*/
private static final String SA_CHECK_ROLE_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckRole";
/**
* Sa-Token 权限校验注解类名。
*/
private static final String SA_CHECK_PERMISSION_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckPermission";
/**
* Sa-Token 忽略校验注解类名。
*/
private static final String SA_IGNORE_CLASS_NAME = BASE_CLASS_NAME + ".SaIgnore";
/**
* Sa-Token 登录校验注解类名。
*/
private static final String SA_CHECK_LOGIN_NAME = BASE_CLASS_NAME + ".SaCheckLogin";
/**
* Sa-Token 角色校验注解类型。
*/
private static final Class<? extends Annotation> SA_CHECK_ROLE_CLASS;
/**
* Sa-Token 权限校验注解类型。
*/
private static final Class<? extends Annotation> SA_CHECK_PERMISSION_CLASS;
/**
* Sa-Token 忽略校验注解类型。
*/
private static final Class<? extends Annotation> SA_IGNORE_CLASS;
/**
* Sa-Token 登录校验注解类型。
*/
private static final Class<? extends Annotation> SA_CHECK_LOGIN_CLASS;
static {
// 通过类加载器去加载注解类Class实例
SA_CHECK_ROLE_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_ROLE_CLASS_NAME, false);
SA_CHECK_PERMISSION_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_PERMISSION_CLASS_NAME, false);
SA_IGNORE_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_IGNORE_CLASS_NAME, false);
SA_CHECK_LOGIN_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_LOGIN_NAME, false);
if (log.isDebugEnabled()) {
log.debug("SaTokenAnnotationJavadocResolver init success, load annotation class: {}", List.of(SA_CHECK_ROLE_CLASS, SA_CHECK_PERMISSION_CLASS, SA_IGNORE_CLASS, SA_CHECK_LOGIN_CLASS));
}
}
/**
* 构造 Sa-Token 权限解析器。
*/
public SaTokenAnnotationMetadataJavadocResolver() {
this(DEFAULT_METADATA_PROVIDER);
}
/**
* 使用自定义元数据提供者创建解析器。
*
* @param metadataProvider 元数据提供者
*/
public SaTokenAnnotationMetadataJavadocResolver(Supplier<SaTokenSecurityMetadata> metadataProvider) {
super(metadataProvider);
}
/**
* 使用指定顺序创建解析器。
*
* @param order 顺序值
*/
public SaTokenAnnotationMetadataJavadocResolver(int order) {
this(DEFAULT_METADATA_PROVIDER, order);
}
/**
* 使用自定义元数据提供者和顺序创建解析器。
*
* @param metadataProvider 元数据提供者
* @param order 顺序值
*/
public SaTokenAnnotationMetadataJavadocResolver(Supplier<SaTokenSecurityMetadata> metadataProvider, int order) {
super(metadataProvider, order);
}
/**
* 判断当前方法是否需要由该解析器处理。
*
* @param handlerMethod Handler 方法
* @return 是否支持
*/
@Override
public boolean supports(HandlerMethod handlerMethod) {
return hasAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS) || hasAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS) || hasAnnotation(handlerMethod, SA_IGNORE_CLASS);
}
/**
* 解析 Sa-Token 注解并转换为文档说明。
*
* @param handlerMethod Handler 方法
* @param operation OpenAPI 操作对象
* @param metadata 权限元数据
* @return Markdown 描述
*/
@Override
public String resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata) {
// 检查是否忽略校验
if (hasAnnotation(handlerMethod, SA_IGNORE_CLASS_NAME)) {
metadata.setIgnore(true);
return metadata.toMarkdownString();
}
// 解析权限校验
resolvePermissionCheck(handlerMethod, metadata);
// 解析角色校验
resolveRoleCheck(handlerMethod, metadata);
return metadata.toMarkdownString();
}
/**
* 解析权限校验
*/
private void resolvePermissionCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
// 解析获取方法上的注解角色信息
if (hasMethodAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS);
resolvePermissionAnnotation(metadata, annotationValueMap);
}
// 解析获取类上的注解角色信息
if (hasClassAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS);
resolvePermissionAnnotation(metadata, annotationValueMap);
}
}
/**
* 解析权限注解
*/
private void resolvePermissionAnnotation(SaTokenSecurityMetadata metadata, Map<String, Object> annotationValueMap) {
try {
// 反射获取注解属性
Object value = annotationValueMap.get("value");
Object mode = annotationValueMap.get("mode");
Object type = annotationValueMap.get("type");
Object orRole = annotationValueMap.get("orRole");
String[] values = Convert.toStrArray(value);
String modeStr = mode != null ? mode.toString() : "AND";
String typeStr = type != null ? type.toString() : "";
String[] orRoles = Convert.toStrArray(orRole);
metadata.addPermission(values, modeStr, typeStr, orRoles);
} catch (Exception ignore) {
// 忽略解析错误
}
}
/**
* 解析角色校验
*/
private void resolveRoleCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
// 解析获取方法上的注解角色信息
if (hasMethodAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS);
resolveRoleAnnotation(metadata, annotationValueMap);
}
// 解析获取类上的注解角色信息
if (hasClassAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS);
resolveRoleAnnotation(metadata, annotationValueMap);
}
}
/**
* 解析角色注解
*/
private void resolveRoleAnnotation(SaTokenSecurityMetadata metadata, Map<String, Object> annotationValueMap) {
try {
// 反射获取注解属性
Object value = annotationValueMap.get("value");
Object mode = annotationValueMap.get("mode");
Object type = annotationValueMap.get("type");
String[] values = Convert.toStrArray(value);
String modeStr = mode != null ? mode.toString() : "AND";
String typeStr = type != null ? type.toString() : "";
metadata.addRole(values, modeStr, typeStr);
} catch (Exception ignore) {
// 忽略解析错误
}
}
}
@@ -0,0 +1 @@
org.dromara.common.doc.config.SpringDocConfig
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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-elasticsearch</artifactId>
<description>
ruoyi-common-elasticsearch ES搜索引擎服务
</description>
<dependencies>
<!-- Easy-Es 搜索引擎 -->
<dependency>
<groupId>org.dromara.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,37 @@
package org.dromara.common.elasticsearch.config;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* 健康检查配置注入
*
* @author Lion Li
*/
public class ActuatorEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
/**
* 根据 easy-es 开关同步设置 Elasticsearch 健康检查开关。
*
* @param environment Spring 环境配置
* @param application Spring 应用实例
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
String enable = environment.getProperty("easy-es.enable", "false");
System.setProperty("management.health.elasticsearch.enabled", enable);
}
/**
* 返回环境后处理器执行顺序。
*
* @return 最高优先级
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
@@ -0,0 +1,17 @@
package org.dromara.common.elasticsearch.config;
import org.dromara.easyes.spring.annotation.EsMapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* easy-es 配置
*
* @author Lion Li
*/
@AutoConfiguration
@ConditionalOnProperty(value = "easy-es.enable", havingValue = "true")
@EsMapperScan("org.dromara.**.esmapper")
public class EasyEsConfiguration {
}
@@ -0,0 +1,2 @@
org.springframework.boot.EnvironmentPostProcessor=\
org.dromara.common.elasticsearch.config.ActuatorEnvironmentPostProcessor
@@ -0,0 +1 @@
org.dromara.common.elasticsearch.config.EasyEsConfiguration
+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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-encrypt</artifactId>
<description>
ruoyi-common-encrypt 数据加解密模块
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- 加密包引入 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<!-- Hutool 加密工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- Spring WebMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- MyBatis Plus 启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,20 @@
package org.dromara.common.encrypt.annotation;
import java.lang.annotation.*;
/**
* 强制加密注解
*
* @author Michelle.Chung
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {
/**
* 响应加密忽略,默认不加密,为 true 时加密
*/
boolean response() default false;
}
@@ -0,0 +1,44 @@
package org.dromara.common.encrypt.annotation;
import org.dromara.common.encrypt.enums.AlgorithmType;
import org.dromara.common.encrypt.enums.EncodeType;
import java.lang.annotation.*;
/**
* 字段加密注解
*
* @author 老马
*/
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
/**
* 加密算法
*/
AlgorithmType algorithm() default AlgorithmType.DEFAULT;
/**
* 秘钥。AES、SM4需要
*/
String password() default "";
/**
* 公钥。RSA、SM2需要
*/
String publicKey() default "";
/**
* 私钥。RSA、SM2需要
*/
String privateKey() default "";
/**
* 编码方式。对加密算法为BASE64的不起作用
*/
EncodeType encode() default EncodeType.DEFAULT;
}
@@ -0,0 +1,46 @@
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.FilterRegistration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* api 解密自动配置
*
* @author wdhcr
*/
@AutoConfiguration
@EnableConfigurationProperties(ApiDecryptProperties.class)
@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
public class ApiDecryptAutoConfiguration {
/**
* 注册 API 加解密过滤器。
*
* @param properties API 解密配置
* @param requestMappingHandlerMapping 请求映射处理器
* @param handlerExceptionResolver 异常处理器
* @return API 加解密过滤器
*/
@Bean
@FilterRegistration(
name = "cryptoFilter",
urlPatterns = "/*",
order = FilterRegistrationBean.HIGHEST_PRECEDENCE,
dispatcherTypes = DispatcherType.REQUEST
)
public CryptoFilter cryptoFilter(ApiDecryptProperties properties,
RequestMappingHandlerMapping requestMappingHandlerMapping,
HandlerExceptionResolver handlerExceptionResolver) {
return new CryptoFilter(properties, requestMappingHandlerMapping, handlerExceptionResolver);
}
}
@@ -0,0 +1,109 @@
package org.dromara.common.encrypt.config;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.core.EncryptContextFactory;
import org.dromara.common.encrypt.core.EncryptedFieldProcessor;
import org.dromara.common.encrypt.core.EncryptorManager;
import org.dromara.common.encrypt.enums.AlgorithmType;
import org.dromara.common.encrypt.interceptor.MybatisDecryptInterceptor;
import org.dromara.common.encrypt.interceptor.MybatisEncryptInterceptor;
import org.dromara.common.encrypt.properties.EncryptorProperties;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* 加解密配置
*
* @author Lion Li
*/
@AutoConfiguration(after = MybatisPlusAutoConfiguration.class)
@EnableConfigurationProperties(EncryptorProperties.class)
@ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true")
@Slf4j
public class EncryptorAutoConfiguration {
@Autowired
private EncryptorProperties properties;
/**
* 创建字段加解密管理器。
*
* @param mybatisPlusProperties MyBatis-Plus 配置
* @return 字段加解密管理器
*/
@Bean
public EncryptorManager encryptorManager(MybatisPlusProperties mybatisPlusProperties) {
validateEncryptorProperties(properties);
return new EncryptorManager(mybatisPlusProperties.getTypeAliasesPackage());
}
/**
* 创建加密上下文工厂。
*
* @return 加密上下文工厂
*/
@Bean
public EncryptContextFactory encryptContextFactory() {
return new EncryptContextFactory(properties);
}
/**
* 创建加密字段处理器。
*
* @param encryptorManager 加解密管理器
* @param encryptContextFactory 加密上下文工厂
* @return 加密字段处理器
*/
@Bean
public EncryptedFieldProcessor encryptedFieldProcessor(EncryptorManager encryptorManager, EncryptContextFactory encryptContextFactory) {
return new EncryptedFieldProcessor(encryptorManager, encryptContextFactory);
}
/**
* 创建 MyBatis 入参加密拦截器。
*
* @param encryptedFieldProcessor 加密字段处理器
* @return MyBatis 入参加密拦截器
*/
@Bean
public MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptedFieldProcessor encryptedFieldProcessor) {
return new MybatisEncryptInterceptor(encryptedFieldProcessor);
}
/**
* 创建 MyBatis 出参解密拦截器。
*
* @param encryptedFieldProcessor 加密字段处理器
* @return MyBatis 出参解密拦截器
*/
@Bean
public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptedFieldProcessor encryptedFieldProcessor) {
return new MybatisDecryptInterceptor(encryptedFieldProcessor);
}
/**
* 校验字段加解密配置完整性。
*
* @param properties 字段加解密配置
*/
private void validateEncryptorProperties(EncryptorProperties properties) {
AlgorithmType algorithm = properties.getAlgorithm();
if (algorithm == AlgorithmType.AES || algorithm == AlgorithmType.SM4) {
if (StringUtils.isBlank(properties.getPassword())) {
throw new IllegalArgumentException("mybatis-encryptor.password 不能为空");
}
}
if (algorithm == AlgorithmType.RSA || algorithm == AlgorithmType.SM2) {
if (StringUtils.isAnyBlank(properties.getPublicKey(), properties.getPrivateKey())) {
throw new IllegalArgumentException("mybatis-encryptor.publicKey 与 privateKey 不能为空");
}
}
}
}
@@ -0,0 +1,41 @@
package org.dromara.common.encrypt.core;
import lombok.Data;
import org.dromara.common.encrypt.enums.AlgorithmType;
import org.dromara.common.encrypt.enums.EncodeType;
/**
* 加密上下文 用于encryptor传递必要的参数。
*
* @author 老马
* @version 4.6.0
*/
@Data
public class EncryptContext {
/**
* 默认算法
*/
private AlgorithmType algorithm;
/**
* 安全秘钥
*/
private String password;
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
/**
* 编码方式,base64/hex
*/
private EncodeType encode;
}
@@ -0,0 +1,45 @@
package org.dromara.common.encrypt.core;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.EncryptField;
import org.dromara.common.encrypt.enums.AlgorithmType;
import org.dromara.common.encrypt.enums.EncodeType;
import org.dromara.common.encrypt.properties.EncryptorProperties;
import java.lang.reflect.Field;
/**
* 加密上下文工厂。
*
* @author Lion Li
*/
public class EncryptContextFactory {
private final EncryptorProperties defaultProperties;
/**
* 构造加密上下文工厂。
*
* @param defaultProperties 默认加密配置
*/
public EncryptContextFactory(EncryptorProperties defaultProperties) {
this.defaultProperties = defaultProperties;
}
/**
* 根据字段注解和默认配置创建加密上下文。
*
* @param field 加密字段
* @return 加密上下文
*/
public EncryptContext create(Field field) {
EncryptField encryptField = field.getAnnotation(EncryptField.class);
EncryptContext encryptContext = new EncryptContext();
encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
encryptContext.setPassword(StringUtils.isBlank(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
encryptContext.setPrivateKey(StringUtils.isBlank(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
encryptContext.setPublicKey(StringUtils.isBlank(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
return encryptContext;
}
}
@@ -0,0 +1,133 @@
package org.dromara.common.encrypt.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.*;
/**
* 加密字段处理器。
*
* @author Lion Li
*/
@Slf4j
public class EncryptedFieldProcessor {
private final EncryptorManager encryptorManager;
private final EncryptContextFactory contextFactory;
/**
* 构造加密字段处理器。
*
* @param encryptorManager 加解密管理器
* @param contextFactory 加密上下文工厂
*/
public EncryptedFieldProcessor(EncryptorManager encryptorManager, EncryptContextFactory contextFactory) {
this.encryptorManager = encryptorManager;
this.contextFactory = contextFactory;
}
/**
* 加密对象字段,并返回原始字段快照。
*
* @param sourceObject 待加密对象
* @return 原始字段快照
*/
public List<FieldSnapshot> encrypt(Object sourceObject) {
List<FieldSnapshot> snapshots = new ArrayList<>();
handle(sourceObject, Collections.newSetFromMap(new IdentityHashMap<>()), (target, field, value) -> {
String encrypt = encryptorManager.encrypt(value, contextFactory.create(field));
if (!Objects.equals(value, encrypt)) {
snapshots.add(new FieldSnapshot(target, field, value));
field.set(target, encrypt);
}
});
return snapshots;
}
/**
* 解密对象字段。
*
* @param sourceObject 待解密对象
*/
public void decrypt(Object sourceObject) {
handle(sourceObject, Collections.newSetFromMap(new IdentityHashMap<>()), (target, field, value) ->
field.set(target, encryptorManager.decrypt(value, contextFactory.create(field))));
}
/**
* 递归处理对象、集合或 Map 中声明加密注解的字段。
*
* @param sourceObject 待处理对象
* @param visited 已访问对象集合
* @param fieldHandler 字段处理回调
*/
private void handle(Object sourceObject, Set<Object> visited, FieldHandler fieldHandler) {
if (ObjectUtil.isNull(sourceObject) || sourceObject instanceof String || visited.contains(sourceObject)) {
return;
}
visited.add(sourceObject);
if (sourceObject instanceof Map<?, ?> map) {
new HashSet<>(map.values()).forEach(value -> handle(value, visited, fieldHandler));
return;
}
if (sourceObject instanceof Collection<?> collection) {
if (CollUtil.isEmpty(collection)) {
return;
}
collection.forEach(item -> handle(item, visited, fieldHandler));
return;
}
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
if (CollUtil.isEmpty(fields)) {
return;
}
try {
for (Field field : fields) {
String value = Convert.toStr(field.get(sourceObject));
if (ObjectUtil.isNotNull(value)) {
fieldHandler.handle(sourceObject, field, value);
}
}
} catch (Exception e) {
log.error("处理加密字段时出错", e);
}
}
/**
* 加密字段处理回调。
*/
@FunctionalInterface
private interface FieldHandler {
/**
* 处理单个加密字段。
*
* @param target 字段所属对象
* @param field 加密字段
* @param value 字段原始字符串值
* @throws IllegalAccessException 字段访问失败时抛出
*/
void handle(Object target, Field field, String value) throws IllegalAccessException;
}
/**
* 字段原始值快照。
*/
public record FieldSnapshot(Object target, Field field, Object value) {
/**
* 恢复原始字段值。
*/
public void restore() {
try {
field.set(target, value);
} catch (IllegalAccessException e) {
log.error("恢复加密字段时出错", e);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More