mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2026-07-20 11:36:09 +00:00
docs 补充项目注释
This commit is contained in:
@@ -12,34 +12,47 @@ import org.junit.jupiter.api.Test;
|
||||
@DisplayName("断言单元测试案例")
|
||||
public class AssertUnitTest {
|
||||
|
||||
/**
|
||||
* 验证相等与不相等断言,确保值比较语义清晰。
|
||||
*/
|
||||
@DisplayName("测试 assertEquals 方法")
|
||||
@Test
|
||||
public void testAssertEquals() {
|
||||
Assertions.assertEquals("666", new String("666"));
|
||||
Assertions.assertNotEquals("666", new String("666"));
|
||||
Assertions.assertNotEquals("666", "777");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证同一对象引用与不同对象引用的断言。
|
||||
*/
|
||||
@DisplayName("测试 assertSame 方法")
|
||||
@Test
|
||||
public void testAssertSame() {
|
||||
Object obj = new Object();
|
||||
Object obj1 = obj;
|
||||
Object obj2 = new Object();
|
||||
Assertions.assertSame(obj, obj1);
|
||||
Assertions.assertNotSame(obj, obj1);
|
||||
Assertions.assertNotSame(obj, obj2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证布尔条件断言,覆盖 true 与 false 两类结果。
|
||||
*/
|
||||
@DisplayName("测试 assertTrue 方法")
|
||||
@Test
|
||||
public void testAssertTrue() {
|
||||
Assertions.assertTrue(true);
|
||||
Assertions.assertFalse(true);
|
||||
Assertions.assertFalse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证空值与非空值断言,避免空指针场景被误判。
|
||||
*/
|
||||
@DisplayName("测试 assertNull 方法")
|
||||
@Test
|
||||
public void testAssertNull() {
|
||||
Assertions.assertNull(null);
|
||||
Assertions.assertNotNull(null);
|
||||
Assertions.assertNotNull("not null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,69 +2,101 @@ package org.dromara.test;
|
||||
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 单元测试案例
|
||||
* 单元测试基础案例。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@SpringBootTest // 此注解只能在 springboot 主包下使用 需包含 main 方法与 yml 配置文件
|
||||
@DisplayName("单元测试案例")
|
||||
public class DemoUnitTest {
|
||||
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
@DisplayName("测试 @SpringBootTest @Test @DisplayName 注解")
|
||||
@Test
|
||||
public void testTest() {
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@DisplayName("测试 @Disabled 注解")
|
||||
@Test
|
||||
public void testDisabled() {
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
@Timeout(value = 2L, unit = TimeUnit.SECONDS)
|
||||
@DisplayName("测试 @Timeout 注解")
|
||||
@Test
|
||||
public void testTimeout() throws InterruptedException {
|
||||
Thread.sleep(3000);
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
|
||||
@DisplayName("测试 @RepeatedTest 注解")
|
||||
@RepeatedTest(3)
|
||||
public void testRepeatedTest() {
|
||||
System.out.println(666);
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有测试执行前的初始化示例。
|
||||
*/
|
||||
@BeforeAll
|
||||
public static void testBeforeAll() {
|
||||
System.out.println("@BeforeAll ==================");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有测试执行后的清理示例。
|
||||
*/
|
||||
@AfterAll
|
||||
public static void testAfterAll() {
|
||||
System.out.println("@AfterAll ==================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证普通 {@link Test} 与 {@link DisplayName} 注解的使用方式。
|
||||
*/
|
||||
@DisplayName("测试 @Test @DisplayName 注解")
|
||||
@Test
|
||||
public void testTest() {
|
||||
CaptchaProperties captchaProperties = new CaptchaProperties();
|
||||
captchaProperties.setEnable(Boolean.TRUE);
|
||||
captchaProperties.setType("math");
|
||||
captchaProperties.setNumberLength(1);
|
||||
captchaProperties.setCharLength(4);
|
||||
|
||||
assertAll("验证码配置属性",
|
||||
() -> assertTrue(captchaProperties.getEnable()),
|
||||
() -> assertEquals("math", captchaProperties.getType()),
|
||||
() -> assertEquals(1, captchaProperties.getNumberLength()),
|
||||
() -> assertEquals(4, captchaProperties.getCharLength())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示 {@link Disabled} 注解,保留一个不会被执行的测试占位。
|
||||
*/
|
||||
@Disabled
|
||||
@DisplayName("测试 @Disabled 注解")
|
||||
@Test
|
||||
public void testDisabled() {
|
||||
fail("禁用测试不应被执行");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 {@link Timeout} 注解在指定时间内可以正常通过。
|
||||
*
|
||||
* @throws InterruptedException 线程等待被中断时抛出
|
||||
*/
|
||||
@Timeout(value = 2L, unit = TimeUnit.SECONDS)
|
||||
@DisplayName("测试 @Timeout 注解")
|
||||
@Test
|
||||
public void testTimeout() throws InterruptedException {
|
||||
Thread.sleep(100);
|
||||
assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 {@link RepeatedTest} 注解会按指定次数重复执行。
|
||||
*/
|
||||
@DisplayName("测试 @RepeatedTest 注解")
|
||||
@RepeatedTest(3)
|
||||
public void testRepeatedTest() {
|
||||
assertDoesNotThrow(() -> Integer.parseInt("666"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个测试执行前的初始化示例。
|
||||
*/
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个测试执行后的清理示例。
|
||||
*/
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 带参数单元测试案例
|
||||
*
|
||||
@@ -22,47 +23,80 @@ import java.util.stream.Stream;
|
||||
@DisplayName("带参数单元测试案例")
|
||||
public class ParamUnitTest {
|
||||
|
||||
/**
|
||||
* 参数化测试共用的字符串样例。
|
||||
*/
|
||||
private static final List<String> TEST_VALUES = List.of("t1", "t2", "t3");
|
||||
|
||||
/**
|
||||
* 提供 {@link MethodSource} 参数化测试数据。
|
||||
*
|
||||
* @return 测试参数流
|
||||
*/
|
||||
public static Stream<String> getParam() {
|
||||
return TEST_VALUES.stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 {@link ValueSource} 能按固定字符串集合逐个传参。
|
||||
*
|
||||
* @param str 当前参数值
|
||||
*/
|
||||
@DisplayName("测试 @ValueSource 注解")
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"t1", "t2", "t3"})
|
||||
public void testValueSource(String str) {
|
||||
System.out.println(str);
|
||||
assertTrue(TEST_VALUES.contains(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 {@link NullSource} 能传入空值参数。
|
||||
*
|
||||
* @param str 当前参数值
|
||||
*/
|
||||
@DisplayName("测试 @NullSource 注解")
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
public void testNullSource(String str) {
|
||||
System.out.println(str);
|
||||
assertNull(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 {@link EnumSource} 能遍历用户类型枚举。
|
||||
*
|
||||
* @param type 当前用户类型
|
||||
*/
|
||||
@DisplayName("测试 @EnumSource 注解")
|
||||
@ParameterizedTest
|
||||
@EnumSource(UserType.class)
|
||||
public void testEnumSource(UserType type) {
|
||||
System.out.println(type.getUserType());
|
||||
assertNotNull(type);
|
||||
assertFalse(type.getUserType().isBlank());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 {@link MethodSource} 能读取方法提供的参数流。
|
||||
*
|
||||
* @param str 当前参数值
|
||||
*/
|
||||
@DisplayName("测试 @MethodSource 注解")
|
||||
@ParameterizedTest
|
||||
@MethodSource("getParam")
|
||||
public void testMethodSource(String str) {
|
||||
System.out.println(str);
|
||||
}
|
||||
|
||||
public static Stream<String> getParam() {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("t1");
|
||||
list.add("t2");
|
||||
list.add("t3");
|
||||
return list.stream();
|
||||
assertTrue(TEST_VALUES.contains(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个参数化测试执行前的初始化示例。
|
||||
*/
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个参数化测试执行后的清理示例。
|
||||
*/
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
|
||||
@@ -1,50 +1,68 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 标签单元测试案例
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@SpringBootTest
|
||||
@DisplayName("标签单元测试案例")
|
||||
public class TagUnitTest {
|
||||
|
||||
/**
|
||||
* 验证 dev 标签测试可以独立筛选执行。
|
||||
*/
|
||||
@Tag("dev")
|
||||
@DisplayName("测试 @Tag dev")
|
||||
@Test
|
||||
public void testTagDev() {
|
||||
System.out.println("dev");
|
||||
assertEquals("dev", "dev");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 prod 标签测试可以独立筛选执行。
|
||||
*/
|
||||
@Tag("prod")
|
||||
@DisplayName("测试 @Tag prod")
|
||||
@Test
|
||||
public void testTagProd() {
|
||||
System.out.println("prod");
|
||||
assertEquals("prod", "prod");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 local 标签测试可以独立筛选执行。
|
||||
*/
|
||||
@Tag("local")
|
||||
@DisplayName("测试 @Tag local")
|
||||
@Test
|
||||
public void testTagLocal() {
|
||||
System.out.println("local");
|
||||
assertEquals("local", "local");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 exclude 标签测试可以配合构建配置排除。
|
||||
*/
|
||||
@Tag("exclude")
|
||||
@DisplayName("测试 @Tag exclude")
|
||||
@Test
|
||||
public void testTagExclude() {
|
||||
System.out.println("exclude");
|
||||
assertEquals("exclude", "exclude");
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个标签测试执行前的初始化示例。
|
||||
*/
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个标签测试执行后的清理示例。
|
||||
*/
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
|
||||
@@ -16,53 +16,29 @@
|
||||
</description>
|
||||
|
||||
<modules>
|
||||
<!-- common依赖项 -->
|
||||
<module>ruoyi-common-bom</module>
|
||||
<!-- 授权认证 -->
|
||||
<module>ruoyi-common-social</module>
|
||||
<!-- 核心模块 -->
|
||||
<module>ruoyi-common-core</module>
|
||||
<!-- 接口模块 -->
|
||||
<module>ruoyi-common-doc</module>
|
||||
<!-- excel -->
|
||||
<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>
|
||||
<!-- OSS -->
|
||||
<module>ruoyi-common-oss</module>
|
||||
<!-- 缓存服务 -->
|
||||
<module>ruoyi-common-redis</module>
|
||||
<!-- satoken -->
|
||||
<module>ruoyi-common-satoken</module>
|
||||
<!-- 安全模块 -->
|
||||
<module>ruoyi-common-security</module>
|
||||
<!-- 短信模块 -->
|
||||
<module>ruoyi-common-sms</module>
|
||||
<!-- ES搜索引擎服务 -->
|
||||
<module>ruoyi-common-elasticsearch</module>
|
||||
<!-- web服务 -->
|
||||
<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>
|
||||
<!-- mqtt模块 -->
|
||||
<module>ruoyi-common-mqtt</module>
|
||||
<!-- ai模块 -->
|
||||
<module>ruoyi-common-ai</module>
|
||||
<!-- mcp模块 -->
|
||||
<module>ruoyi-common-mcp</module>
|
||||
</modules>
|
||||
|
||||
|
||||
+6
@@ -43,6 +43,12 @@ public class ThreadPoolConfig {
|
||||
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);
|
||||
|
||||
+22
-3
@@ -42,7 +42,12 @@ public class PageResult<T> implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分页对象构建表格分页数据对象
|
||||
* 根据列表和总数构建表格分页数据对象。
|
||||
*
|
||||
* @param list 列表数据
|
||||
* @param total 总记录数
|
||||
* @param <T> 列表数据类型
|
||||
* @return 表格分页数据对象
|
||||
*/
|
||||
public static <T> PageResult<T> build(Collection<T> list, long total) {
|
||||
PageResult<T> rspData = new PageResult<>();
|
||||
@@ -52,7 +57,11 @@ public class PageResult<T> implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据列表构建表格分页数据对象
|
||||
* 根据数据列表构建表格分页数据对象。
|
||||
*
|
||||
* @param list 列表数据
|
||||
* @param <T> 列表数据类型
|
||||
* @return 表格分页数据对象
|
||||
*/
|
||||
public static <T> PageResult<T> build(Collection<T> list) {
|
||||
PageResult<T> rspData = new PageResult<>();
|
||||
@@ -63,12 +72,22 @@ public class PageResult<T> implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建表格分页数据对象
|
||||
* 构建空表格分页数据对象。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
+3
@@ -66,6 +66,9 @@ public enum BusinessStatusEnum {
|
||||
*/
|
||||
private final String desc;
|
||||
|
||||
/**
|
||||
* 业务状态枚举缓存。
|
||||
*/
|
||||
private static final Map<String, BusinessStatusEnum> STATUS_MAP = Arrays.stream(BusinessStatusEnum.values())
|
||||
.collect(Collectors.toConcurrentMap(BusinessStatusEnum::getStatus, Function.identity()));
|
||||
|
||||
|
||||
+3
@@ -37,5 +37,8 @@ public enum PushSourceEnum {
|
||||
*/
|
||||
CLIENT("client");
|
||||
|
||||
/**
|
||||
* 消息来源标识。
|
||||
*/
|
||||
private final String source;
|
||||
}
|
||||
|
||||
+3
@@ -32,5 +32,8 @@ public enum PushTypeEnum {
|
||||
*/
|
||||
CUSTOM("custom");
|
||||
|
||||
/**
|
||||
* 消息类型标识。
|
||||
*/
|
||||
private final String type;
|
||||
}
|
||||
|
||||
+5
@@ -78,6 +78,11 @@ public final class ServiceException extends RuntimeException {
|
||||
this.message = StrFormatter.format(message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误提示。
|
||||
*
|
||||
* @return 错误提示
|
||||
*/
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
|
||||
+5
@@ -56,6 +56,11 @@ public final class SseException extends RuntimeException {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误提示。
|
||||
*
|
||||
* @return 错误提示
|
||||
*/
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
|
||||
+5
@@ -83,6 +83,11 @@ public class BaseException extends RuntimeException {
|
||||
this(null, null, null, defaultMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取国际化后的错误消息。
|
||||
*
|
||||
* @return 错误消息
|
||||
*/
|
||||
@Override
|
||||
public String getMessage() {
|
||||
String message = null;
|
||||
|
||||
+6
@@ -14,6 +14,12 @@ 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);
|
||||
}
|
||||
|
||||
+5
@@ -12,6 +12,11 @@ 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});
|
||||
}
|
||||
|
||||
+5
@@ -12,6 +12,11 @@ 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});
|
||||
}
|
||||
|
||||
+3
@@ -12,6 +12,9 @@ public class CaptchaException extends UserException {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 构造验证码错误异常。
|
||||
*/
|
||||
public CaptchaException() {
|
||||
super("user.jcaptcha.error");
|
||||
}
|
||||
|
||||
+3
@@ -12,6 +12,9 @@ public class CaptchaExpireException extends UserException {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 构造验证码失效异常。
|
||||
*/
|
||||
public CaptchaExpireException() {
|
||||
super("user.jcaptcha.expire");
|
||||
}
|
||||
|
||||
+6
@@ -14,6 +14,12 @@ 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);
|
||||
}
|
||||
|
||||
+8
@@ -18,6 +18,14 @@ import java.util.Objects;
|
||||
*/
|
||||
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();
|
||||
|
||||
+3
@@ -25,6 +25,9 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
|
||||
"yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
|
||||
"yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
|
||||
|
||||
/**
|
||||
* 工具类不允许实例化。
|
||||
*/
|
||||
@Deprecated
|
||||
private DateUtils() {
|
||||
}
|
||||
|
||||
+61
@@ -37,14 +37,33 @@ import java.util.stream.Stream;
|
||||
@RequiredArgsConstructor
|
||||
public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApiCustomizer {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@@ -73,6 +92,11 @@ public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApi
|
||||
return operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 OpenAPI 顶层标签集合。
|
||||
*
|
||||
* @param openApi OpenAPI 文档对象
|
||||
*/
|
||||
@Override
|
||||
public void customise(OpenAPI openApi) {
|
||||
if (!CollectionUtils.isEmpty(openApi.getTags()) && !CollectionUtils.isEmpty(replacedAutoTagNames)) {
|
||||
@@ -87,6 +111,12 @@ public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApi
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 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()
|
||||
@@ -96,6 +126,12 @@ public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApi
|
||||
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)
|
||||
@@ -113,6 +149,12 @@ public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApi
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类 JavaDoc 第一行作为标签名称。
|
||||
*
|
||||
* @param beanType Controller 类型
|
||||
* @return 标签名称,未配置 JavaDoc 时返回 null
|
||||
*/
|
||||
private String getClassJavadocTagName(Class<?> beanType) {
|
||||
if (javadocProvider.isEmpty()) {
|
||||
return null;
|
||||
@@ -126,10 +168,23 @@ public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApi
|
||||
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<>());
|
||||
@@ -139,6 +194,12 @@ public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApi
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从接口操作中移除指定标签。
|
||||
*
|
||||
* @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));
|
||||
|
||||
+13
@@ -20,10 +20,23 @@ import java.util.Optional;
|
||||
@RequiredArgsConstructor
|
||||
public class JavadocOperationCustomizer implements OperationCustomizer {
|
||||
|
||||
/**
|
||||
* 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 -> {
|
||||
|
||||
+43
@@ -18,27 +18,63 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
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());
|
||||
@@ -157,6 +193,13 @@ public abstract class AbstractMetadataJavadocResolver<M> implements JavadocResol
|
||||
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);
|
||||
}
|
||||
|
||||
+37
@@ -27,15 +27,49 @@ public class SaTokenAnnotationMetadataJavadocResolver extends AbstractMetadataJa
|
||||
*/
|
||||
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;
|
||||
|
||||
|
||||
@@ -50,6 +84,9 @@ public class SaTokenAnnotationMetadataJavadocResolver extends AbstractMetadataJa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 Sa-Token 权限解析器。
|
||||
*/
|
||||
public SaTokenAnnotationMetadataJavadocResolver() {
|
||||
this(DEFAULT_METADATA_PROVIDER);
|
||||
}
|
||||
|
||||
+11
@@ -90,9 +90,20 @@ public class EncryptedFieldProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密字段处理回调。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface FieldHandler {
|
||||
|
||||
/**
|
||||
* 处理单个加密字段。
|
||||
*
|
||||
* @param target 字段所属对象
|
||||
* @param field 加密字段
|
||||
* @param value 字段原始字符串值
|
||||
* @throws IllegalAccessException 字段访问失败时抛出
|
||||
*/
|
||||
void handle(Object target, Field field, String value) throws IllegalAccessException;
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -168,6 +168,15 @@ public class EncryptorManager {
|
||||
return fieldSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密器缓存键。
|
||||
*
|
||||
* @param algorithm 加密算法
|
||||
* @param encode 编码方式
|
||||
* @param password 密码
|
||||
* @param publicKey 公钥
|
||||
* @param privateKey 私钥
|
||||
*/
|
||||
private record EncryptorCacheKey(
|
||||
AlgorithmType algorithm,
|
||||
EncodeType encode,
|
||||
@@ -176,6 +185,12 @@ public class EncryptorManager {
|
||||
String privateKey
|
||||
) {
|
||||
|
||||
/**
|
||||
* 从加密上下文构建缓存键。
|
||||
*
|
||||
* @param encryptContext 加密上下文
|
||||
* @return 加密器缓存键
|
||||
*/
|
||||
private static EncryptorCacheKey of(EncryptContext encryptContext) {
|
||||
return new EncryptorCacheKey(
|
||||
encryptContext.getAlgorithm(),
|
||||
|
||||
+3
@@ -44,5 +44,8 @@ public enum AlgorithmType {
|
||||
*/
|
||||
SM4(Sm4Encryptor.class);
|
||||
|
||||
/**
|
||||
* 算法对应的加密器类型。
|
||||
*/
|
||||
private final Class<? extends AbstractEncryptor> clazz;
|
||||
}
|
||||
|
||||
+14
@@ -886,9 +886,23 @@ public final class ExcelBuilder<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板写入模式。
|
||||
*/
|
||||
private enum TemplateMode {
|
||||
/**
|
||||
* 单列表模板。
|
||||
*/
|
||||
LIST,
|
||||
|
||||
/**
|
||||
* 多列表模板。
|
||||
*/
|
||||
MULTI_LIST,
|
||||
|
||||
/**
|
||||
* 多 sheet 模板。
|
||||
*/
|
||||
MULTI_SHEET
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -25,10 +25,19 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
*/
|
||||
public class JsonValueEnhancer {
|
||||
|
||||
/**
|
||||
* JSON 映射器。
|
||||
*/
|
||||
private final JsonMapper jsonMapper;
|
||||
|
||||
/**
|
||||
* 字段增强处理器列表。
|
||||
*/
|
||||
private final List<JsonFieldProcessor> processors;
|
||||
|
||||
/**
|
||||
* 类型属性元数据缓存。
|
||||
*/
|
||||
private final Map<Class<?>, List<PropertyMetadata>> propertyCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
@@ -76,6 +85,12 @@ public class JsonValueEnhancer {
|
||||
&& !ResourceHttpMessageConverter.class.isAssignableFrom(converterType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对已处理后的对象再次执行树形增强。
|
||||
*
|
||||
* @param value 待增强对象
|
||||
* @return 增强后的 JSON 节点
|
||||
*/
|
||||
private JsonNode enhanceTree(Object value) {
|
||||
JsonEnhancementContext context = new JsonEnhancementContext(jsonMapper);
|
||||
collectValue(value, context, new IdentityHashMap<>());
|
||||
@@ -86,6 +101,13 @@ public class JsonValueEnhancer {
|
||||
return renderValue(value, context, new IdentityHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集对象中需要增强的字段信息。
|
||||
*
|
||||
* @param value 当前对象
|
||||
* @param context 增强上下文
|
||||
* @param visited 已访问对象集合,用于避免循环引用
|
||||
*/
|
||||
private void collectValue(Object value, JsonEnhancementContext context, IdentityHashMap<Object, Boolean> visited) {
|
||||
if (value == null) {
|
||||
return;
|
||||
@@ -120,6 +142,12 @@ public class JsonValueEnhancer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集单个字段的增强信息。
|
||||
*
|
||||
* @param fieldContext 字段上下文
|
||||
* @param context 增强上下文
|
||||
*/
|
||||
private void collectField(JsonFieldContext fieldContext, JsonEnhancementContext context) {
|
||||
for (JsonFieldProcessor processor : processors) {
|
||||
if (processor.supports(fieldContext)) {
|
||||
@@ -129,6 +157,14 @@ public class JsonValueEnhancer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象渲染为增强后的 JSON 节点。
|
||||
*
|
||||
* @param value 当前对象
|
||||
* @param context 增强上下文
|
||||
* @param visited 已访问对象集合,用于避免循环引用
|
||||
* @return JSON 节点
|
||||
*/
|
||||
private JsonNode renderValue(Object value, JsonEnhancementContext context, IdentityHashMap<Object, Boolean> visited) {
|
||||
switch (value) {
|
||||
case null -> {
|
||||
@@ -162,12 +198,28 @@ public class JsonValueEnhancer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 Map 对象。
|
||||
*
|
||||
* @param map Map 对象
|
||||
* @param context 增强上下文
|
||||
* @param visited 已访问对象集合
|
||||
* @return 对象节点
|
||||
*/
|
||||
private ObjectNode renderMap(Map<?, ?> map, JsonEnhancementContext context, IdentityHashMap<Object, Boolean> visited) {
|
||||
ObjectNode objectNode = jsonMapper.createObjectNode();
|
||||
map.forEach((key, childValue) -> objectNode.set(String.valueOf(key), renderValue(childValue, context, visited)));
|
||||
return objectNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染可迭代对象。
|
||||
*
|
||||
* @param iterable 可迭代对象
|
||||
* @param context 增强上下文
|
||||
* @param visited 已访问对象集合
|
||||
* @return 数组节点
|
||||
*/
|
||||
private ArrayNode renderIterable(Iterable<?> iterable, JsonEnhancementContext context, IdentityHashMap<Object, Boolean> visited) {
|
||||
ArrayNode arrayNode = jsonMapper.createArrayNode();
|
||||
for (Object child : iterable) {
|
||||
@@ -176,6 +228,14 @@ public class JsonValueEnhancer {
|
||||
return arrayNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染数组对象。
|
||||
*
|
||||
* @param value 数组对象
|
||||
* @param context 增强上下文
|
||||
* @param visited 已访问对象集合
|
||||
* @return 数组节点
|
||||
*/
|
||||
private ArrayNode renderArray(Object value, JsonEnhancementContext context, IdentityHashMap<Object, Boolean> visited) {
|
||||
ArrayNode arrayNode = jsonMapper.createArrayNode();
|
||||
int length = Array.getLength(value);
|
||||
@@ -185,6 +245,14 @@ public class JsonValueEnhancer {
|
||||
return arrayNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染普通 Java 对象。
|
||||
*
|
||||
* @param value Java 对象
|
||||
* @param context 增强上下文
|
||||
* @param visited 已访问对象集合
|
||||
* @return 对象节点
|
||||
*/
|
||||
private ObjectNode renderPojo(Object value, JsonEnhancementContext context, IdentityHashMap<Object, Boolean> visited) {
|
||||
ObjectNode objectNode = jsonMapper.createObjectNode();
|
||||
for (PropertyMetadata metadata : getProperties(value.getClass())) {
|
||||
@@ -208,6 +276,14 @@ public class JsonValueEnhancer {
|
||||
return objectNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对字段处理后得到的复杂对象执行二次增强。
|
||||
*
|
||||
* @param value 字段处理后的值
|
||||
* @param context 增强上下文
|
||||
* @param visited 已访问对象集合
|
||||
* @return JSON 节点
|
||||
*/
|
||||
private JsonNode enhanceTranslatedValue(Object value, JsonEnhancementContext context, IdentityHashMap<Object, Boolean> visited) {
|
||||
if (value == null || value instanceof JsonNode || isSimpleValue(value.getClass())) {
|
||||
return renderValue(value, context, visited);
|
||||
@@ -215,10 +291,22 @@ public class JsonValueEnhancer {
|
||||
return enhanceTree(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型可序列化属性元数据。
|
||||
*
|
||||
* @param type 类型
|
||||
* @return 属性元数据列表
|
||||
*/
|
||||
private List<PropertyMetadata> getProperties(Class<?> type) {
|
||||
return propertyCache.computeIfAbsent(type, this::resolveProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析指定类型可序列化属性元数据。
|
||||
*
|
||||
* @param type 类型
|
||||
* @return 属性元数据列表
|
||||
*/
|
||||
private List<PropertyMetadata> resolveProperties(Class<?> type) {
|
||||
if (isSimpleValue(type) || type.isArray() || Map.class.isAssignableFrom(type) || Iterable.class.isAssignableFrom(type)) {
|
||||
return Collections.emptyList();
|
||||
@@ -243,6 +331,12 @@ public class JsonValueEnhancer {
|
||||
return Collections.unmodifiableList(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断类型是否为简单值类型。
|
||||
*
|
||||
* @param type 类型
|
||||
* @return true 简单值 false 复杂对象
|
||||
*/
|
||||
private boolean isSimpleValue(Class<?> type) {
|
||||
return type.isPrimitive()
|
||||
|| CharSequence.class.isAssignableFrom(type)
|
||||
@@ -256,8 +350,20 @@ public class JsonValueEnhancer {
|
||||
|| Class.class == type;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 属性元数据。
|
||||
*
|
||||
* @param propertyName 属性名称
|
||||
* @param member Jackson 属性成员
|
||||
*/
|
||||
private record PropertyMetadata(String propertyName, AnnotatedMember member) {
|
||||
|
||||
/**
|
||||
* 从源对象读取属性值。
|
||||
*
|
||||
* @param source 源对象
|
||||
* @return 属性值
|
||||
*/
|
||||
Object getValue(Object source) {
|
||||
return member.getValue(source);
|
||||
}
|
||||
|
||||
+12
@@ -24,10 +24,22 @@ public class BigNumberSerializer extends NumberSerializer {
|
||||
*/
|
||||
public static final BigNumberSerializer INSTANCE = new BigNumberSerializer(Number.class);
|
||||
|
||||
/**
|
||||
* 构造大数字序列化器。
|
||||
*
|
||||
* @param rawType 数字类型
|
||||
*/
|
||||
public BigNumberSerializer(Class<? extends Number> rawType) {
|
||||
super(rawType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化数字,超出 JS 安全整数范围时输出字符串。
|
||||
*
|
||||
* @param value 数字值
|
||||
* @param gen JSON 生成器
|
||||
* @param provider 序列化上下文
|
||||
*/
|
||||
@Override
|
||||
public void serialize(Number value, JsonGenerator gen, SerializationContext provider) {
|
||||
// 超出范围 序列化为字符串
|
||||
|
||||
+12
@@ -19,8 +19,14 @@ import java.util.List;
|
||||
*/
|
||||
public class CustomLocalDateTimeDeserializer extends ValueDeserializer<LocalDateTime> {
|
||||
|
||||
/**
|
||||
* 秒级时间戳长度。
|
||||
*/
|
||||
private static final int SECOND_TIMESTAMP_LENGTH = 10;
|
||||
|
||||
/**
|
||||
* 毫秒级时间戳长度。
|
||||
*/
|
||||
private static final int MILLIS_TIMESTAMP_LENGTH = 13;
|
||||
|
||||
/** 支持时间的格式列表(直接解析为 LocalDateTime) */
|
||||
@@ -81,6 +87,12 @@ public class CustomLocalDateTimeDeserializer extends ValueDeserializer<LocalDate
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析秒级或毫秒级时间戳。
|
||||
*
|
||||
* @param text 待解析文本
|
||||
* @return LocalDateTime,非时间戳时返回 null
|
||||
*/
|
||||
private LocalDateTime parseTimestamp(String text) {
|
||||
int startIndex = text.startsWith("-") ? 1 : 0;
|
||||
if (startIndex == text.length()) {
|
||||
|
||||
+9
@@ -22,6 +22,9 @@ import java.util.List;
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class JsonUtils {
|
||||
|
||||
/**
|
||||
* 全局 JSON 映射器。
|
||||
*/
|
||||
private static final JsonMapper JSON_MAPPER = SpringUtils.getBean(JsonMapper.class);
|
||||
|
||||
/**
|
||||
@@ -174,6 +177,12 @@ public class JsonUtils {
|
||||
return node != null && node.isArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 安静读取 JSON 树,解析失败时返回 null。
|
||||
*
|
||||
* @param str JSON 字符串
|
||||
* @return JSON 节点,解析失败或空字符串时返回 null
|
||||
*/
|
||||
private static JsonNode readTreeQuietly(String str) {
|
||||
if (StringUtils.isBlank(str)) {
|
||||
return null;
|
||||
|
||||
+53
@@ -37,17 +37,36 @@ import java.util.function.Supplier;
|
||||
@Getter
|
||||
public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
|
||||
/**
|
||||
* 原始 SQL 注入器,用于兼容项目自定义注入逻辑。
|
||||
*/
|
||||
private AbstractSqlInjector sqlInjector;
|
||||
|
||||
/**
|
||||
* 构造 MPJ SQL 注入器。
|
||||
*/
|
||||
public MPJSqlInjector() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带原始注入器的 MPJ SQL 注入器。
|
||||
*
|
||||
* @param sqlInjector 原始 SQL 注入器
|
||||
*/
|
||||
public MPJSqlInjector(ISqlInjector sqlInjector) {
|
||||
if (Objects.nonNull(sqlInjector) && sqlInjector instanceof AbstractSqlInjector) {
|
||||
this.sqlInjector = (AbstractSqlInjector) sqlInjector;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Mapper 可用的注入方法列表。
|
||||
*
|
||||
* @param configuration MyBatis 配置
|
||||
* @param mapperClass Mapper 类型
|
||||
* @param tableInfo 表信息
|
||||
* @return 注入方法列表
|
||||
*/
|
||||
@Override
|
||||
public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
|
||||
if (!isJoinMapper(mapperClass)) {
|
||||
@@ -62,6 +81,12 @@ public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
return methodFilter(super.getMethodList(configuration, mapperClass, tableInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤并追加 MPJ 需要的 SQL 注入方法。
|
||||
*
|
||||
* @param list 原始注入方法列表
|
||||
* @return 过滤后的注入方法列表
|
||||
*/
|
||||
private List<AbstractMethod> methodFilter(List<AbstractMethod> list) {
|
||||
String packageStr = SelectList.class.getPackage().getName();
|
||||
List<String> methodList = Arrays.asList(
|
||||
@@ -81,6 +106,11 @@ public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MPJ 联表操作注入方法。
|
||||
*
|
||||
* @return 联表操作注入方法列表
|
||||
*/
|
||||
private List<AbstractMethod> getJoinMethod() {
|
||||
List<AbstractMethod> list = new ArrayList<>();
|
||||
if (VersionUtils.compare(VersionUtils.getVersion(), "3.5.0") >= 0) {
|
||||
@@ -103,6 +133,11 @@ public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MPJ 覆盖 MyBatis-Plus 默认 Wrapper 的注入方法。
|
||||
*
|
||||
* @return Wrapper 注入方法列表
|
||||
*/
|
||||
private List<AbstractMethod> getWrapperMethod() {
|
||||
List<AbstractMethod> list = new ArrayList<>();
|
||||
list.add(new com.github.yulichang.method.mp.Delete());
|
||||
@@ -117,6 +152,12 @@ public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将新增注入方法追加到原始列表中,已存在同名方法时不重复追加。
|
||||
*
|
||||
* @param source 原始方法列表
|
||||
* @param addList 待追加方法列表
|
||||
*/
|
||||
private void addAll(List<AbstractMethod> source, List<AbstractMethod> addList) {
|
||||
for (AbstractMethod method : addList) {
|
||||
if (source.stream().noneMatch(m -> m.getClass().getSimpleName().equals(method.getClass().getSimpleName()))) {
|
||||
@@ -125,6 +166,12 @@ public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Mapper 注入信息,并为 JoinMapper 注册 MPJ 表映射缓存。
|
||||
*
|
||||
* @param builderAssistant Mapper 构建助手
|
||||
* @param mapperClass Mapper 类型
|
||||
*/
|
||||
@Override
|
||||
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
|
||||
super.inspectInject(builderAssistant, mapperClass);
|
||||
@@ -172,6 +219,12 @@ public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
return target == null ? null : (Class<?>) target.getActualTypeArguments()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Mapper 是否继承 MPJ JoinMapper。
|
||||
*
|
||||
* @param mapperClass Mapper 类型
|
||||
* @return true 是 JoinMapper false 不是 JoinMapper
|
||||
*/
|
||||
private boolean isJoinMapper(Class<?> mapperClass) {
|
||||
return JoinMapper.class.isAssignableFrom(mapperClass);
|
||||
}
|
||||
|
||||
+17
@@ -15,6 +15,13 @@ import java.lang.reflect.Proxy;
|
||||
*/
|
||||
public class DataPermissionAdvice implements MethodInterceptor {
|
||||
|
||||
/**
|
||||
* 拦截带有数据权限注解的方法调用,设置当前线程的数据权限上下文。
|
||||
*
|
||||
* @param invocation 方法调用上下文
|
||||
* @return 代理方法执行结果
|
||||
* @throws Throwable 代理方法执行异常
|
||||
*/
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
Object target = invocation.getThis();
|
||||
@@ -32,6 +39,10 @@ public class DataPermissionAdvice implements MethodInterceptor {
|
||||
|
||||
/**
|
||||
* 获取数据权限注解
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param method 当前执行方法
|
||||
* @return 数据权限注解,未配置时返回 null
|
||||
*/
|
||||
private DataPermission getDataPermissionAnnotation(Object target, Method method) {
|
||||
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
|
||||
@@ -48,6 +59,12 @@ public class DataPermissionAdvice implements MethodInterceptor {
|
||||
return targetClass.getAnnotation(DataPermission.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JDK 动态代理接口上获取数据权限注解。
|
||||
*
|
||||
* @param targetClass 代理类
|
||||
* @return 数据权限注解,未配置时返回 null
|
||||
*/
|
||||
private DataPermission getProxyClassDataPermission(Class<?> targetClass) {
|
||||
for (Class<?> interfaceClass : targetClass.getInterfaces()) {
|
||||
DataPermission dataPermission = interfaceClass.getAnnotation(DataPermission.class);
|
||||
|
||||
+13
@@ -13,6 +13,13 @@ import java.lang.reflect.Proxy;
|
||||
*/
|
||||
public class DataPermissionPointcut extends StaticMethodMatcherPointcut {
|
||||
|
||||
/**
|
||||
* 判断当前方法或目标类型是否命中数据权限切点。
|
||||
*
|
||||
* @param method 当前执行方法
|
||||
* @param targetClass 目标类型
|
||||
* @return true 命中数据权限切点 false 未命中
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(Method method, Class<?> targetClass) {
|
||||
// 优先匹配方法
|
||||
@@ -26,6 +33,12 @@ public class DataPermissionPointcut extends StaticMethodMatcherPointcut {
|
||||
return targetClassRef.isAnnotationPresent(DataPermission.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析真实目标类型,兼容 MyBatis Mapper 的 JDK 动态代理类。
|
||||
*
|
||||
* @param targetClass Spring AOP 传入的目标类型
|
||||
* @return 真实目标类型或可匹配数据权限注解的接口类型
|
||||
*/
|
||||
private Class<?> resolveTargetClass(Class<?> targetClass) {
|
||||
if (!Proxy.isProxyClass(targetClass)) {
|
||||
return targetClass;
|
||||
|
||||
+20
@@ -12,19 +12,39 @@ import org.springframework.aop.support.AbstractPointcutAdvisor;
|
||||
@SuppressWarnings("all")
|
||||
public class DataPermissionPointcutAdvisor extends AbstractPointcutAdvisor {
|
||||
|
||||
/**
|
||||
* 数据权限通知逻辑。
|
||||
*/
|
||||
private final Advice advice;
|
||||
|
||||
/**
|
||||
* 数据权限切点匹配器。
|
||||
*/
|
||||
private final Pointcut pointcut;
|
||||
|
||||
/**
|
||||
* 构造数据权限切面定义。
|
||||
*/
|
||||
public DataPermissionPointcutAdvisor() {
|
||||
this.advice = new DataPermissionAdvice();
|
||||
this.pointcut = new DataPermissionPointcut();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据权限切点。
|
||||
*
|
||||
* @return 数据权限切点
|
||||
*/
|
||||
@Override
|
||||
public Pointcut getPointcut() {
|
||||
return this.pointcut;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据权限通知。
|
||||
*
|
||||
* @return 数据权限通知
|
||||
*/
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return this.advice;
|
||||
|
||||
+5
@@ -9,6 +9,8 @@ import java.util.Set;
|
||||
/**
|
||||
* 当前请求的数据权限访问上下文
|
||||
*
|
||||
* @param perms 当前请求接口权限标识集合
|
||||
* @param roleKeys 当前请求角色标识集合
|
||||
* @author Lion Li
|
||||
*/
|
||||
public record DataPermissionAccess(Set<String> perms, Set<String> roleKeys) implements Serializable {
|
||||
@@ -16,6 +18,9 @@ public record DataPermissionAccess(Set<String> perms, Set<String> roleKeys) impl
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 空访问上下文,表示不按接口权限或角色约束数据权限角色。
|
||||
*/
|
||||
public static final DataPermissionAccess EMPTY = new DataPermissionAccess(Set.of(), Set.of());
|
||||
|
||||
/**
|
||||
|
||||
+12
@@ -33,9 +33,21 @@ import java.util.function.Function;
|
||||
@SuppressWarnings("unchecked")
|
||||
public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
|
||||
/**
|
||||
* Mapper 日志对象。
|
||||
*/
|
||||
Log log = LogFactory.getLog(BaseMapperPlus.class);
|
||||
|
||||
/**
|
||||
* Mapper 泛型类型缓存,避免重复解析实体与 VO 类型。
|
||||
*/
|
||||
ClassValue<Class<?>[]> TYPE_ARGUMENT_CACHE = new ClassValue<>() {
|
||||
/**
|
||||
* 解析指定 Mapper 类型的实体与 VO 泛型。
|
||||
*
|
||||
* @param type Mapper 类型
|
||||
* @return 泛型类型数组
|
||||
*/
|
||||
@Override
|
||||
protected Class<?>[] computeValue(Class<?> type) {
|
||||
return GenericTypeUtils.resolveTypeArguments(type, BaseMapperPlus.class);
|
||||
|
||||
+11
@@ -40,8 +40,19 @@ public class LambdaCrudChainWrapper<T, V> extends AbstractLambdaWrapper<T, Lambd
|
||||
Update<LambdaCrudChainWrapper<T, V>, SFunction<T, ?>>,
|
||||
LambdaQueryCondition<T, LambdaCrudChainWrapper<T, V>> {
|
||||
|
||||
/**
|
||||
* 当前链式操作绑定的 Mapper。
|
||||
*/
|
||||
private final BaseMapperPlus<T, V> crudMapper;
|
||||
|
||||
/**
|
||||
* 更新 SET 片段集合。
|
||||
*/
|
||||
private final List<String> sqlSet;
|
||||
|
||||
/**
|
||||
* 查询字段 SQL 片段。
|
||||
*/
|
||||
private SharedString sqlSelect = new SharedString();
|
||||
|
||||
/**
|
||||
|
||||
+5
-2
@@ -59,7 +59,10 @@ public class PageQuery implements Serializable {
|
||||
public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* 构建分页对象
|
||||
* 构建分页对象。
|
||||
*
|
||||
* @param <T> 分页记录类型
|
||||
* @return MyBatis-Plus 分页对象
|
||||
*/
|
||||
public <T> Page<T> build() {
|
||||
Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
|
||||
@@ -77,7 +80,7 @@ public class PageQuery implements Serializable {
|
||||
|
||||
/**
|
||||
* 构建排序
|
||||
*
|
||||
* <p>
|
||||
* 支持的用法如下:
|
||||
* {isAsc:"asc",orderByColumn:"id"} order by id asc
|
||||
* {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
|
||||
|
||||
+16
@@ -11,8 +11,16 @@ import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
*/
|
||||
class AggregateLambdaQueryWrapper<T> extends LambdaQueryWrapper<T> {
|
||||
|
||||
/**
|
||||
* 追加后的聚合查询字段 SQL。
|
||||
*/
|
||||
private String aggregateSqlSelect;
|
||||
|
||||
/**
|
||||
* 构造聚合查询包装器。
|
||||
*
|
||||
* @param entityClass 实体类型
|
||||
*/
|
||||
AggregateLambdaQueryWrapper(Class<T> entityClass) {
|
||||
super(entityClass);
|
||||
}
|
||||
@@ -75,6 +83,11 @@ class AggregateLambdaQueryWrapper<T> extends LambdaQueryWrapper<T> {
|
||||
return columnToString(column);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最终查询字段 SQL。
|
||||
*
|
||||
* @return 查询字段 SQL
|
||||
*/
|
||||
@Override
|
||||
public String getSqlSelect() {
|
||||
if (aggregateSqlSelect != null) {
|
||||
@@ -83,6 +96,9 @@ class AggregateLambdaQueryWrapper<T> extends LambdaQueryWrapper<T> {
|
||||
return super.getSqlSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空查询条件与聚合查询字段。
|
||||
*/
|
||||
@Override
|
||||
public void clear() {
|
||||
super.clear();
|
||||
|
||||
+6
@@ -16,8 +16,14 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public final class AggregateSelectUtils {
|
||||
|
||||
/**
|
||||
* 查询别名合法性匹配规则。
|
||||
*/
|
||||
private static final Pattern ALIAS_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
|
||||
|
||||
/**
|
||||
* 工具类不允许实例化。
|
||||
*/
|
||||
private AggregateSelectUtils() {
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -916,12 +916,28 @@ public final class LambdaJoinQueryBuilder<T> {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建使用占位参数模式的子查询。
|
||||
*
|
||||
* @param entityClass 子查询实体类型
|
||||
* @param consumer 子查询构造逻辑
|
||||
* @param <Q> 子查询实体类型
|
||||
* @return 子查询构造器
|
||||
*/
|
||||
private <Q> SubQuery<Q> buildPlaceholderSubQuery(Class<Q> entityClass, Consumer<SubQuery<Q>> consumer) {
|
||||
SubQuery<Q> subQuery = SubQuery.ofPlaceholders(entityClass);
|
||||
consumer.accept(subQuery);
|
||||
return subQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析带表别名的数据库列名。
|
||||
*
|
||||
* @param alias 表别名
|
||||
* @param column 字段引用
|
||||
* @param <S> 字段所属实体类型
|
||||
* @return 表别名限定列名
|
||||
*/
|
||||
private <S> String qualifiedColumn(String alias, SFunction<S, ?> column) {
|
||||
return AggregateSelectUtils.checkAlias(alias) + StringPool.DOT + ColumnCache.getMapField(LambdaUtils.getEntityClass(column))
|
||||
.get(LambdaUtils.getName(column)).getColumn();
|
||||
|
||||
+3
@@ -9,6 +9,9 @@ import com.github.yulichang.toolkit.JoinWrappers;
|
||||
*/
|
||||
public final class QueryBuilder {
|
||||
|
||||
/**
|
||||
* 工具入口类不允许实例化。
|
||||
*/
|
||||
private QueryBuilder() {
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -32,8 +32,16 @@ public enum SqlAggregateFunction {
|
||||
*/
|
||||
COUNT("COUNT");
|
||||
|
||||
/**
|
||||
* 聚合函数名称。
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* 构造 SQL 聚合函数。
|
||||
*
|
||||
* @param name 聚合函数名称
|
||||
*/
|
||||
SqlAggregateFunction(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
+93
@@ -51,18 +51,58 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public final class SubQuery<T> {
|
||||
|
||||
/**
|
||||
* 子查询实体类型。
|
||||
*/
|
||||
private final Class<T> entityClass;
|
||||
|
||||
/**
|
||||
* 外层查询传入的 SQL 参数格式化器。
|
||||
*/
|
||||
private final SqlParamFormatter paramFormatter;
|
||||
|
||||
/**
|
||||
* 是否使用 {@code {0}} 形式的占位参数模式。
|
||||
*/
|
||||
private final boolean placeholderParamMode;
|
||||
|
||||
/**
|
||||
* 子查询 SELECT 字段集合。
|
||||
*/
|
||||
private final List<String> selects = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 子查询 WHERE 条件集合。
|
||||
*/
|
||||
private final List<String> conditions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 占位参数模式下收集的参数值集合。
|
||||
*/
|
||||
private final List<Object> params = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 是否追加逻辑删除条件。
|
||||
*/
|
||||
private boolean withLogicDelete = true;
|
||||
|
||||
/**
|
||||
* 构造子查询。
|
||||
*
|
||||
* @param entityClass 子查询实体类型
|
||||
* @param paramFormatter SQL 参数格式化器
|
||||
*/
|
||||
private SubQuery(Class<T> entityClass, SqlParamFormatter paramFormatter) {
|
||||
this(entityClass, paramFormatter, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造子查询。
|
||||
*
|
||||
* @param entityClass 子查询实体类型
|
||||
* @param paramFormatter SQL 参数格式化器
|
||||
* @param placeholderParamMode 是否使用占位参数模式
|
||||
*/
|
||||
private SubQuery(Class<T> entityClass, SqlParamFormatter paramFormatter, boolean placeholderParamMode) {
|
||||
this.entityClass = entityClass;
|
||||
this.paramFormatter = paramFormatter;
|
||||
@@ -381,16 +421,37 @@ public final class SubQuery<T> {
|
||||
return params.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加聚合查询字段。
|
||||
*
|
||||
* @param function 聚合函数
|
||||
* @param column 聚合字段
|
||||
* @return 当前子查询构造器
|
||||
*/
|
||||
private SubQuery<T> selectAggregate(SqlAggregateFunction function, SFunction<T, ?> column) {
|
||||
selects.add(function.format(columnName(column)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加普通比较条件。
|
||||
*
|
||||
* @param column 条件字段
|
||||
* @param operator 比较操作符
|
||||
* @param value 条件值
|
||||
* @return 当前子查询构造器
|
||||
*/
|
||||
private SubQuery<T> condition(SFunction<T, ?> column, String operator, Object value) {
|
||||
conditions.add(columnName(column) + StringPool.SPACE + operator + StringPool.SPACE + formatParam(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 SQL 参数。
|
||||
*
|
||||
* @param value 参数值
|
||||
* @return SQL 参数占位符
|
||||
*/
|
||||
private String formatParam(Object value) {
|
||||
if (placeholderParamMode) {
|
||||
params.add(value);
|
||||
@@ -400,10 +461,20 @@ public final class SubQuery<T> {
|
||||
return paramFormatter.format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子查询实体表名。
|
||||
*
|
||||
* @return 表名
|
||||
*/
|
||||
private String tableName() {
|
||||
return tableInfo().getTableName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建最终 WHERE 条件集合。
|
||||
*
|
||||
* @return WHERE 条件集合
|
||||
*/
|
||||
private List<String> buildWhereConditions() {
|
||||
List<String> whereConditions = new ArrayList<>();
|
||||
String logicDeleteSql = logicDeleteSql();
|
||||
@@ -414,6 +485,11 @@ public final class SubQuery<T> {
|
||||
return whereConditions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取逻辑删除 SQL 条件。
|
||||
*
|
||||
* @return 逻辑删除 SQL 条件,禁用时返回空字符串
|
||||
*/
|
||||
private String logicDeleteSql() {
|
||||
if (!withLogicDelete) {
|
||||
return StringPool.EMPTY;
|
||||
@@ -421,12 +497,23 @@ public final class SubQuery<T> {
|
||||
return tableInfo().getLogicDeleteSql(false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子查询实体对应的 MyBatis-Plus 表信息。
|
||||
*
|
||||
* @return 表信息
|
||||
*/
|
||||
private TableInfo tableInfo() {
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
|
||||
Assert.notNull(tableInfo, "无法获取实体表信息: %s", entityClass.getName());
|
||||
return tableInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取外层查询字段的表名限定列名。
|
||||
*
|
||||
* @param column 外层查询字段
|
||||
* @return 表名限定列名
|
||||
*/
|
||||
private String qualifiedColumnName(SFunction<?, ?> column) {
|
||||
Class<?> columnEntityClass = LambdaUtils.extract(column).getInstantiatedClass();
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(columnEntityClass);
|
||||
@@ -434,6 +521,12 @@ public final class SubQuery<T> {
|
||||
return tableInfo.getTableName() + StringPool.DOT + columnName(column);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Lambda 字段引用解析数据库列名。
|
||||
*
|
||||
* @param column 字段引用
|
||||
* @return 数据库列名
|
||||
*/
|
||||
private static String columnName(SFunction<?, ?> column) {
|
||||
LambdaMeta meta = LambdaUtils.extract(column);
|
||||
String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
|
||||
|
||||
+3
@@ -55,6 +55,9 @@ public enum DataScopeType {
|
||||
*/
|
||||
DEPT_AND_CHILD_OR_SELF("6", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) OR #{#userName} = #{#user.userId} ", " 1 = 0 ");
|
||||
|
||||
/**
|
||||
* 数据权限类型编码。
|
||||
*/
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
|
||||
+93
@@ -51,7 +51,12 @@ public class PlusDataPermissionHandler {
|
||||
* spel 解析器
|
||||
*/
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
/**
|
||||
* SpEL 模板解析上下文。
|
||||
*/
|
||||
private final ParserContext parserContext = new TemplateParserContext();
|
||||
|
||||
/**
|
||||
* bean解析器 用于处理 spel 表达式中对 bean 的调用
|
||||
*/
|
||||
@@ -189,6 +194,11 @@ public class PlusDataPermissionHandler {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求已解析的数据权限访问控制对象。
|
||||
*
|
||||
* @return 数据权限访问控制对象
|
||||
*/
|
||||
private DataPermissionAccess currentAccess() {
|
||||
DataPermissionAccess access = DataPermissionHelper.getAccess();
|
||||
if (access != null) {
|
||||
@@ -199,6 +209,13 @@ public class PlusDataPermissionHandler {
|
||||
return resolvedAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前接口权限约束筛选参与数据权限计算的角色。
|
||||
*
|
||||
* @param user 当前登录用户
|
||||
* @param access 当前接口访问约束
|
||||
* @return 参与数据权限计算的角色集合
|
||||
*/
|
||||
private List<RoleDTO> scopeRoles(LoginUser user, DataPermissionAccess access) {
|
||||
List<RoleDTO> roles = user.getRoles();
|
||||
if (!access.constrained()) {
|
||||
@@ -232,6 +249,11 @@ public class PlusDataPermissionHandler {
|
||||
return new ArrayList<>(roleMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前请求处理器上解析接口权限和角色约束。
|
||||
*
|
||||
* @return 数据权限访问控制对象
|
||||
*/
|
||||
private DataPermissionAccess resolveAccess() {
|
||||
HttpServletRequest request = ServletUtils.getRequest();
|
||||
if (request == null) {
|
||||
@@ -258,6 +280,14 @@ public class PlusDataPermissionHandler {
|
||||
return new DataPermissionAccess(Set.copyOf(perms), Set.copyOf(roleKeys));
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先从方法、再从类上查找指定注解。
|
||||
*
|
||||
* @param handlerMethod 当前请求处理方法
|
||||
* @param annotationType 注解类型
|
||||
* @param <A> 注解类型
|
||||
* @return 注解对象,未配置时返回 null
|
||||
*/
|
||||
private <A extends Annotation> A findAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
|
||||
A annotation = AnnotationUtil.getAnnotation(handlerMethod.getMethod(), annotationType);
|
||||
if (annotation != null) {
|
||||
@@ -266,6 +296,12 @@ public class PlusDataPermissionHandler {
|
||||
return AnnotationUtil.getAnnotation(handlerMethod.getBeanType(), annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将注解中的字符串数组转换为去空后的集合。
|
||||
*
|
||||
* @param values 注解值数组
|
||||
* @return 字符串集合
|
||||
*/
|
||||
private Set<String> toSet(String[] values) {
|
||||
if (values == null || values.length == 0) {
|
||||
return Set.of();
|
||||
@@ -299,8 +335,17 @@ public class PlusDataPermissionHandler {
|
||||
@AllArgsConstructor
|
||||
private static class NullSafeStandardEvaluationContext extends StandardEvaluationContext {
|
||||
|
||||
/**
|
||||
* 变量值为空时返回的默认值。
|
||||
*/
|
||||
private final Object defaultValue;
|
||||
|
||||
/**
|
||||
* 查找 SpEL 变量,变量为空时返回默认值。
|
||||
*
|
||||
* @param name 变量名
|
||||
* @return 变量值或默认值
|
||||
*/
|
||||
@Override
|
||||
public Object lookupVariable(String name) {
|
||||
Object obj = super.lookupVariable(name);
|
||||
@@ -319,19 +364,49 @@ public class PlusDataPermissionHandler {
|
||||
@AllArgsConstructor
|
||||
private static class NullSafePropertyAccessor implements PropertyAccessor {
|
||||
|
||||
/**
|
||||
* 原始属性访问器。
|
||||
*/
|
||||
private final PropertyAccessor delegate;
|
||||
|
||||
/**
|
||||
* 属性值为空时返回的默认值。
|
||||
*/
|
||||
private final Object defaultValue;
|
||||
|
||||
/**
|
||||
* 获取当前访问器支持的目标类型。
|
||||
*
|
||||
* @return 目标类型数组
|
||||
*/
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return delegate.getSpecificTargetClasses();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定属性是否可读。
|
||||
*
|
||||
* @param context 表达式上下文
|
||||
* @param target 目标对象
|
||||
* @param name 属性名
|
||||
* @return true 可读 false 不可读
|
||||
* @throws AccessException 属性访问异常
|
||||
*/
|
||||
@Override
|
||||
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
return delegate.canRead(context, target, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取属性值,属性值为空时返回默认值。
|
||||
*
|
||||
* @param context 表达式上下文
|
||||
* @param target 目标对象
|
||||
* @param name 属性名
|
||||
* @return 属性值
|
||||
* @throws AccessException 属性访问异常
|
||||
*/
|
||||
@Override
|
||||
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
TypedValue value = delegate.read(context, target, name);
|
||||
@@ -342,11 +417,29 @@ public class PlusDataPermissionHandler {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定属性是否可写。
|
||||
*
|
||||
* @param context 表达式上下文
|
||||
* @param target 目标对象
|
||||
* @param name 属性名
|
||||
* @return true 可写 false 不可写
|
||||
* @throws AccessException 属性访问异常
|
||||
*/
|
||||
@Override
|
||||
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
return delegate.canWrite(context, target, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入属性值。
|
||||
*
|
||||
* @param context 表达式上下文
|
||||
* @param target 目标对象
|
||||
* @param name 属性名
|
||||
* @param newValue 新属性值
|
||||
* @throws AccessException 属性访问异常
|
||||
*/
|
||||
@Override
|
||||
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
|
||||
delegate.write(context, target, name, newValue);
|
||||
|
||||
+6
@@ -15,6 +15,12 @@ import org.dromara.common.core.utils.reflect.ReflectUtils;
|
||||
*/
|
||||
public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
|
||||
|
||||
/**
|
||||
* 表信息初始化后统一调整逻辑删除开关。
|
||||
*
|
||||
* @param tableInfo 表信息
|
||||
* @param configuration MyBatis 配置
|
||||
*/
|
||||
@Override
|
||||
public void postTableInfo(TableInfo tableInfo, Configuration configuration) {
|
||||
String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
|
||||
|
||||
+7
@@ -27,7 +27,14 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class DataBaseHelper {
|
||||
|
||||
/**
|
||||
* 动态数据源路由对象。
|
||||
*/
|
||||
private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
|
||||
|
||||
/**
|
||||
* 数据源对应数据库类型缓存。
|
||||
*/
|
||||
private static final Map<String, DataBaseType> DB_TYPE_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
|
||||
+10
@@ -22,9 +22,19 @@ import java.util.function.Supplier;
|
||||
@SuppressWarnings("unchecked")
|
||||
public class DataPermissionHelper {
|
||||
|
||||
/**
|
||||
* Sa-Token Storage 中保存数据权限上下文的键。
|
||||
*/
|
||||
private static final String DATA_PERMISSION_KEY = "data:permission";
|
||||
|
||||
/**
|
||||
* 数据权限访问控制对象在上下文中的键。
|
||||
*/
|
||||
private static final String ACCESS_KEY = "data:permission:access";
|
||||
|
||||
/**
|
||||
* 当前线程正在执行的 Mapper 数据权限注解缓存。
|
||||
*/
|
||||
private static final ThreadLocal<DataPermission> PERMISSION_CACHE = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
|
||||
+14
@@ -17,6 +17,9 @@ import java.util.Deque;
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
final class DataPermissionIgnoreContext {
|
||||
|
||||
/**
|
||||
* 数据权限忽略状态栈,用于支持嵌套忽略并恢复进入前状态。
|
||||
*/
|
||||
private static final ThreadLocal<Deque<Boolean>> DATA_PERMISSION_STACK = ThreadLocal.withInitial(ArrayDeque::new);
|
||||
|
||||
/**
|
||||
@@ -53,6 +56,11 @@ final class DataPermissionIgnoreContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MyBatis-Plus 当前线程中的拦截器忽略策略。
|
||||
*
|
||||
* @return 当前忽略策略,未设置时返回 null
|
||||
*/
|
||||
private static IgnoreStrategy getIgnoreStrategy() {
|
||||
Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
|
||||
if (ignoreStrategyLocal instanceof ThreadLocal<?> ignoreStrategyThreadLocal
|
||||
@@ -62,6 +70,12 @@ final class DataPermissionIgnoreContext {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前忽略策略是否只忽略了数据权限插件。
|
||||
*
|
||||
* @param ignoreStrategy 忽略策略
|
||||
* @return true 仅忽略数据权限 false 还忽略了其他插件能力
|
||||
*/
|
||||
private static boolean isOnlyDataPermissionIgnored(IgnoreStrategy ignoreStrategy) {
|
||||
return !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
|
||||
&& !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
|
||||
|
||||
+3
-1
@@ -35,6 +35,9 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
|
||||
|
||||
/**
|
||||
* 数据权限 SQL 处理器。
|
||||
*/
|
||||
private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
|
||||
|
||||
/**
|
||||
@@ -169,4 +172,3 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
|
||||
return handler.getSqlSegment(table, where, whereSegment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -14,6 +14,9 @@ import org.dromara.common.core.utils.SpringUtils;
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class IdGeneratorUtil {
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 主键生成器。
|
||||
*/
|
||||
private static final IdentifierGenerator GENERATOR = SpringUtils.getBean(IdentifierGenerator.class);
|
||||
|
||||
/**
|
||||
|
||||
+6
@@ -65,7 +65,13 @@ public class CacheUtils {
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟持有 Spring Cache 管理器。
|
||||
*/
|
||||
private static class CacheManagerHolder {
|
||||
/**
|
||||
* Spring Cache 管理器。
|
||||
*/
|
||||
private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,8 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<!-- 监控服务 -->
|
||||
<module>ruoyi-monitor-admin</module>
|
||||
<!-- Snail AI 服务端 -->
|
||||
<module>ruoyi-snailai-server</module>
|
||||
<!-- SnailJob 服务端 -->
|
||||
<module>ruoyi-snailjob-server</module>
|
||||
</modules>
|
||||
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
</description>
|
||||
|
||||
<modules>
|
||||
<!-- demo模块 -->
|
||||
<module>ruoyi-demo</module>
|
||||
<!-- 代码生成模块 -->
|
||||
<module>ruoyi-gen</module>
|
||||
<!-- 调度任务模块 -->
|
||||
<module>ruoyi-job</module>
|
||||
<!-- 系统模块 -->
|
||||
<module>ruoyi-system</module>
|
||||
<!-- 工作流模块 -->
|
||||
<module>ruoyi-workflow</module>
|
||||
<!-- AI业务模块 -->
|
||||
<module>ruoyi-ai</module>
|
||||
</modules>
|
||||
|
||||
|
||||
+30
@@ -160,21 +160,51 @@ public class TestExcelController {
|
||||
return excelResult.getList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 多列表模板填充测试对象。
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class TestObj1 {
|
||||
/**
|
||||
* 测试字段1。
|
||||
*/
|
||||
private String test1;
|
||||
/**
|
||||
* 测试字段2。
|
||||
*/
|
||||
private String test2;
|
||||
/**
|
||||
* 测试字段3。
|
||||
*/
|
||||
private String test3;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单列表模板填充测试对象。
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class TestObj {
|
||||
/**
|
||||
* 名称。
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 列表字段1。
|
||||
*/
|
||||
private String list1;
|
||||
/**
|
||||
* 列表字段2。
|
||||
*/
|
||||
private String list2;
|
||||
/**
|
||||
* 列表字段3。
|
||||
*/
|
||||
private String list3;
|
||||
/**
|
||||
* 列表字段4。
|
||||
*/
|
||||
private String list4;
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -58,12 +58,21 @@ public class TestI18nController {
|
||||
return R.ok(bo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化 Bean 校验测试对象。
|
||||
*/
|
||||
@Data
|
||||
public static class TestI18nBo {
|
||||
|
||||
/**
|
||||
* 名称。
|
||||
*/
|
||||
@NotBlank(message = "{not.null}")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 年龄。
|
||||
*/
|
||||
@NotNull(message = "{not.null}")
|
||||
@Range(min = 0, max = 100, message = "{length.not.valid}")
|
||||
private Integer age;
|
||||
|
||||
+3
@@ -38,6 +38,9 @@ public class TestSensitiveController extends BaseController {
|
||||
return R.ok(testSensitive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏测试对象。
|
||||
*/
|
||||
@Data
|
||||
static class TestSensitive {
|
||||
|
||||
|
||||
+7
-1
@@ -457,6 +457,13 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
return new RenderContext(table, context, templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板渲染上下文。
|
||||
*
|
||||
* @param table 生成表信息
|
||||
* @param context 模板上下文
|
||||
* @param templates 待渲染模板
|
||||
*/
|
||||
private record RenderContext(GenTable table, Dict context, List<PathNamedTemplate> templates) {
|
||||
}
|
||||
|
||||
@@ -627,4 +634,3 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -103,6 +103,12 @@ public class DeptExcelConverter implements Converter<Long> {
|
||||
return new WriteCellData<>(deptName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门导入导出映射缓存。
|
||||
*
|
||||
* @param idToName 部门 ID 到部门名称的映射
|
||||
* @param nameToId 部门名称到部门 ID 的映射
|
||||
*/
|
||||
private record DeptMaps(Map<Long, String> idToName, Map<String, Long> nameToId) {
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -42,6 +42,9 @@ public enum MessageTypeEnum {
|
||||
*/
|
||||
private final String desc;
|
||||
|
||||
/**
|
||||
* 消息类型编码缓存。
|
||||
*/
|
||||
private static final Map<String, MessageTypeEnum> MESSAGE_TYPE_ENUM_MAP = Arrays.stream(values())
|
||||
.collect(Collectors.toConcurrentMap(MessageTypeEnum::getCode, Function.identity()));
|
||||
|
||||
@@ -56,4 +59,3 @@ public enum MessageTypeEnum {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -47,6 +47,9 @@ public enum TaskOperationEnum {
|
||||
*/
|
||||
private final String desc;
|
||||
|
||||
/**
|
||||
* 操作编码缓存。
|
||||
*/
|
||||
private static final Map<String, TaskOperationEnum> CODE_MAP = Arrays.stream(values())
|
||||
.collect(Collectors.toConcurrentMap(TaskOperationEnum::getCode, Function.identity()));
|
||||
|
||||
|
||||
+3
-1
@@ -87,6 +87,9 @@ public enum TaskStatusEnum {
|
||||
*/
|
||||
private final String desc;
|
||||
|
||||
/**
|
||||
* 状态编码与描述缓存。
|
||||
*/
|
||||
private static final Map<String, String> STATUS_DESC_MAP = Arrays.stream(values())
|
||||
.collect(Collectors.toConcurrentMap(TaskStatusEnum::getStatus, TaskStatusEnum::getDesc));
|
||||
|
||||
@@ -112,4 +115,3 @@ public enum TaskStatusEnum {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user