docs 补充项目注释

This commit is contained in:
AprilWind
2026-06-01 11:50:59 +08:00
parent 941c0b9032
commit 107d3326b4
66 changed files with 1079 additions and 110 deletions
@@ -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 ==================");
-24
View File
@@ -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>
@@ -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);
@@ -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;
}
@@ -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()));
@@ -37,5 +37,8 @@ public enum PushSourceEnum {
*/
CLIENT("client");
/**
* 消息来源标识。
*/
private final String source;
}
@@ -32,5 +32,8 @@ public enum PushTypeEnum {
*/
CUSTOM("custom");
/**
* 消息类型标识。
*/
private final String type;
}
@@ -78,6 +78,11 @@ public final class ServiceException extends RuntimeException {
this.message = StrFormatter.format(message, args);
}
/**
* 获取错误提示。
*
* @return 错误提示
*/
@Override
public String getMessage() {
return message;
@@ -56,6 +56,11 @@ public final class SseException extends RuntimeException {
this.code = code;
}
/**
* 获取错误提示。
*
* @return 错误提示
*/
@Override
public String getMessage() {
return message;
@@ -83,6 +83,11 @@ public class BaseException extends RuntimeException {
this(null, null, null, defaultMessage);
}
/**
* 获取国际化后的错误消息。
*
* @return 错误消息
*/
@Override
public String getMessage() {
String message = null;
@@ -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);
}
@@ -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});
}
@@ -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});
}
@@ -12,6 +12,9 @@ public class CaptchaException extends UserException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造验证码错误异常。
*/
public CaptchaException() {
super("user.jcaptcha.error");
}
@@ -12,6 +12,9 @@ public class CaptchaExpireException extends UserException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 构造验证码失效异常。
*/
public CaptchaExpireException() {
super("user.jcaptcha.expire");
}
@@ -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);
}
@@ -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();
@@ -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() {
}
@@ -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));
@@ -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 -> {
@@ -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);
}
@@ -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);
}
@@ -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;
}
@@ -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(),
@@ -44,5 +44,8 @@ public enum AlgorithmType {
*/
SM4(Sm4Encryptor.class);
/**
* 算法对应的加密器类型
*/
private final Class<? extends AbstractEncryptor> clazz;
}
@@ -886,9 +886,23 @@ public final class ExcelBuilder<T> {
}
}
/**
* 模板写入模式
*/
private enum TemplateMode {
/**
* 单列表模板
*/
LIST,
/**
* 多列表模板
*/
MULTI_LIST,
/**
* sheet 模板
*/
MULTI_SHEET
}
}
@@ -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);
}
@@ -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) {
// 超出范围 序列化为字符串
@@ -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()) {
@@ -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;
@@ -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);
}
@@ -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,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;
@@ -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;
@@ -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());
/**
@@ -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);
@@ -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();
/**
@@ -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
@@ -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();
@@ -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() {
}
@@ -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();
@@ -9,6 +9,9 @@ import com.github.yulichang.toolkit.JoinWrappers;
*/
public final class QueryBuilder {
/**
* 工具入口类不允许实例化
*/
private QueryBuilder() {
}
@@ -32,8 +32,16 @@ public enum SqlAggregateFunction {
*/
COUNT("COUNT");
/**
* 聚合函数名称
*/
private final String name;
/**
* 构造 SQL 聚合函数
*
* @param name 聚合函数名称
*/
SqlAggregateFunction(String name) {
this.name = name;
}
@@ -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());
@@ -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;
/**
@@ -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);
@@ -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");
@@ -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<>();
/**
@@ -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<>();
/**
@@ -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())
@@ -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);
}
}
@@ -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);
/**
@@ -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);
}
-3
View File
@@ -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>
-6
View File
@@ -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>
@@ -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;
}
@@ -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;
@@ -38,6 +38,9 @@ public class TestSensitiveController extends BaseController {
return R.ok(testSensitive);
}
/**
* 脱敏测试对象
*/
@Data
static class TestSensitive {
@@ -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 {
}
}
@@ -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) {
}
@@ -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 {
}
}
@@ -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()));
@@ -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 {
}
}