diff --git a/.run/ruoyi-monitor-admin.run.xml b/.run/ruoyi-monitor-admin.run.xml index beaf7c8fb..11d875a43 100644 --- a/.run/ruoyi-monitor-admin.run.xml +++ b/.run/ruoyi-monitor-admin.run.xml @@ -2,7 +2,7 @@ - diff --git a/.run/ruoyi-server.run.xml b/.run/ruoyi-server.run.xml index 10116af1e..86969f59d 100644 --- a/.run/ruoyi-server.run.xml +++ b/.run/ruoyi-server.run.xml @@ -2,7 +2,7 @@ - diff --git a/.run/ruoyi-snailjob-server.run.xml b/.run/ruoyi-snailjob-server.run.xml index 38ab7745f..e1cbd6a12 100644 --- a/.run/ruoyi-snailjob-server.run.xml +++ b/.run/ruoyi-snailjob-server.run.xml @@ -2,7 +2,7 @@ - diff --git a/README.md b/README.md index bd86ca5e6..98dc2db3f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE) [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.4.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) +[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]() [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]() [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]() @@ -27,7 +27,7 @@ > 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
> 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)
-> 文档地址: [plus-doc](https://plus-doc.dromara.org) 文档在华为云上如果打不开大概率是DNS问题 可以尝试切换网络等方式(或者科学上网) +> 文档地址: [plus-doc](https://plus-doc.dromara.org) 国内加速: [plus-doc.top](https://plus-doc.top) ## 赞助商 @@ -37,7 +37,10 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc
**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/**
Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11
-[如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group) +aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong
+Ruoyi-Plus-Uniapp - https://ruoyi.plus
+ +[如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group) # 本框架与RuoYi的功能差异 diff --git a/pom.xml b/pom.xml index f8dab42db..81e1fe8ab 100644 --- a/pom.xml +++ b/pom.xml @@ -13,28 +13,28 @@ Dromara RuoYi-Vue-Plus多租户管理系统 - 5.4.1 - 3.4.7 + 5.5.0 + 3.5.6 UTF-8 UTF-8 17 3.5.16 - 2.8.8 + 2.8.13 0.15.0 - 1.2.0 + 1.3.0 2.3 1.44.0 - 3.5.12 + 3.5.14 3.9.1 - 5.8.38 - 3.4.7 - 3.50.0 + 5.8.40 + 3.5.3 + 3.51.0 2.2.7 4.3.1 - 1.5.0 - 1.4.8 + 1.8.0 + 1.5.0 0.2.0 - 1.18.36 + 1.18.40 1.80 1.16.7 @@ -42,13 +42,13 @@ 2.28.22 - 3.3.4 + 3.3.5 1.2.83 8.7.2-20250603 - 1.7.4 + 1.8.1 3.4.2 @@ -118,25 +118,6 @@ import - - - org.dromara.warm - warm-flow-mybatis-plus-sb3-starter - ${warm-flow.version} - - - org.dromara.warm - warm-flow-plugin-ui-sb-web - ${warm-flow.version} - - - - - me.zhyd.oauth - JustAuth - ${justauth.version} - - org.dromara @@ -313,6 +294,25 @@ ${mapstruct-plus.version} + + + org.dromara.warm + warm-flow-mybatis-plus-sb3-starter + ${warm-flow.version} + + + org.dromara.warm + warm-flow-plugin-ui-sb-web + ${warm-flow.version} + + + + + me.zhyd.oauth + JustAuth + ${justauth.version} + + org.lionsoul diff --git a/ruoyi-admin/Dockerfile b/ruoyi-admin/Dockerfile index fc13f4ff7..0394ccb97 100644 --- a/ruoyi-admin/Dockerfile +++ b/ruoyi-admin/Dockerfile @@ -1,6 +1,6 @@ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ -FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds -#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds +FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds +#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds #FROM findepi/graalvm:java17-native LABEL maintainer="Lion Li" diff --git a/ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java b/ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java index 7fbc57f95..89b9ab63c 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java @@ -21,6 +21,8 @@ import org.dromara.common.core.domain.model.SocialLoginBody; import org.dromara.common.core.utils.*; import org.dromara.common.encrypt.annotation.ApiEncrypt; import org.dromara.common.json.utils.JsonUtils; +import org.dromara.common.ratelimiter.annotation.RateLimiter; +import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.social.config.properties.SocialLoginConfigProperties; import org.dromara.common.social.config.properties.SocialProperties; @@ -198,6 +200,7 @@ public class AuthController { * * @return 租户列表 */ + @RateLimiter(time = 60, count = 20, limitType = LimitType.IP) @GetMapping("/tenant/list") public R tenantList(HttpServletRequest request) throws Exception { // 返回对象 diff --git a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java index 0848170af..dcd04c5e1 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java @@ -131,15 +131,18 @@ public class CaptchaController { String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; // 生成验证码 CaptchaType captchaType = captchaProperties.getType(); - boolean isMath = CaptchaType.MATH == captchaType; - Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength(); - CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length); + CodeGenerator codeGenerator; + if (CaptchaType.MATH == captchaType) { + codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false); + } else { + codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength()); + } AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); captcha.setGenerator(codeGenerator); captcha.createCode(); // 如果是数学验证码,使用SpEL表达式处理验证码结果 String code = captcha.getCode(); - if (isMath) { + if (CaptchaType.MATH == captchaType) { ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(StringUtils.remove(code, "=")); code = exp.getValue(String.class); diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java b/ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java index 567906e42..5a3351d05 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java @@ -87,7 +87,7 @@ public class SysRegisterService { recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); throw new CaptchaExpireException(); } - if (!code.equalsIgnoreCase(captcha)) { + if (!StringUtils.equalsIgnoreCase(code, captcha)) { recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); throw new CaptchaException(); } diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java index e579f9969..abf590b36 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java @@ -102,7 +102,7 @@ public class PasswordAuthStrategy implements IAuthStrategy { loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); throw new CaptchaExpireException(); } - if (!code.equalsIgnoreCase(captcha)) { + if (!StringUtils.equalsIgnoreCase(code, captcha)) { loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); throw new CaptchaException(); } diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 5dd2e00fe..1b52fab96 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -27,8 +27,6 @@ snail-job: port: 2${server.port} # 客户端ip指定 host: - # RPC类型: netty, grpc - rpc-type: grpc --- # 数据源配置 spring: diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 435a9e856..d77ddf57c 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -30,8 +30,6 @@ snail-job: port: 2${server.port} # 客户端ip指定 host: - # RPC类型: netty, grpc - rpc-type: grpc --- # 数据源配置 spring: diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 03cb2f987..866b8f1a2 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -57,6 +57,13 @@ spring: # 开启虚拟线程 仅jdk21可用 virtual: enabled: false + task: + execution: + # 从 springboot 3.5 开始 spring自带线程池 + # 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用 + thread-name-prefix: async- + # 由spring自己初始化线程池 + mode: force # 资源信息 messages: # 国际化资源文件路径 @@ -127,6 +134,7 @@ tenant: - sys_user_role - sys_client - sys_oss_config + - flow_spel # MyBatisPlus配置 # https://baomidou.com/config/ @@ -189,13 +197,6 @@ springdoc: name: Lion Li email: crazylionli@163.com url: https://gitee.com/dromara/RuoYi-Vue-Plus - components: - # 鉴权方式配置 - security-schemes: - apiKey: - type: APIKEY - in: HEADER - name: ${sa-token.token-name} #这里定义了两个分组,可定义多个,也可以不定义 group-configs: - group: 1.演示模块 @@ -213,20 +214,10 @@ springdoc: xss: # 过滤开关 enabled: true - # 排除链接(多个用逗号分隔) + # 排除链接 excludeUrls: - /system/notice -# 全局线程池相关配置 -# 如使用JDK21请直接使用虚拟线程 不要开启此配置 -thread-pool: - # 是否开启线程池 - enabled: false - # 队列最大长度 - queueCapacity: 128 - # 线程池维护线程所允许的空闲时间 - keepAliveSeconds: 300 - --- # 分布式锁 lock4j 全局配置 lock4j: # 获取分布式锁超时时间,默认为 3000 毫秒 @@ -266,13 +257,9 @@ warm-flow: enabled: true # 是否开启设计器ui ui: true + # 是否显示流程图顶部文字 + top-text-show: true + # 是否渲染节点悬浮提示,默认true + node-tooltip: true # 默认Authorization,如果有多个token,用逗号分隔 token-name: ${sa-token.token-name},clientid - # 流程状态对应的三元色 - chart-status-color: - ## 未办理 - - 62,62,62 - ## 待办理 - - 255,205,23 - ## 已办理 - - 157,255,0 diff --git a/ruoyi-admin/src/main/resources/i18n/messages.properties b/ruoyi-admin/src/main/resources/i18n/messages.properties index cce11c85d..f2777f77b 100644 --- a/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间 user.password.not.blank=用户密码不能为空 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 user.password.not.valid=* 5-50个字符 +user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符 user.email.not.valid=邮箱格式错误 user.email.not.blank=邮箱不能为空 user.phonenumber.not.blank=用户手机号不能为空 diff --git a/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties b/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties index f948c4ab8..306a48f6a 100644 --- a/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties +++ b/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties @@ -17,6 +17,7 @@ user.username.length.valid=Account length must be between {min} and {max} charac user.password.not.blank=Password cannot be empty user.password.length.valid=Password length must be between {min} and {max} characters user.password.not.valid=* 5-50 characters +user.password.format.valid=Password must contain uppercase, lowercase, digit, and special character user.email.not.valid=Mailbox format error user.email.not.blank=Mailbox cannot be blank user.phonenumber.not.blank=Phone number cannot be blank diff --git a/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties b/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties index cce11c85d..f2777f77b 100644 --- a/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties +++ b/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties @@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间 user.password.not.blank=用户密码不能为空 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 user.password.not.valid=* 5-50个字符 +user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符 user.email.not.valid=邮箱格式错误 user.email.not.blank=邮箱不能为空 user.phonenumber.not.blank=用户手机号不能为空 diff --git a/ruoyi-admin/src/main/resources/ip2region.xdb b/ruoyi-admin/src/main/resources/ip2region.xdb index 7052c0571..6f86c7d9b 100644 Binary files a/ruoyi-admin/src/main/resources/ip2region.xdb and b/ruoyi-admin/src/main/resources/ip2region.xdb differ diff --git a/ruoyi-admin/src/main/resources/logback-plus.xml b/ruoyi-admin/src/main/resources/logback-plus.xml index b74289ecd..2d786f9a9 100644 --- a/ruoyi-admin/src/main/resources/logback-plus.xml +++ b/ruoyi-admin/src/main/resources/logback-plus.xml @@ -38,7 +38,7 @@ - ${log.path}/sys-info.%d{yyyy-MM-dd}.log + ${log.path}/sys-info.%d{yyyy-MM-dd}.log.gz 60 @@ -60,7 +60,7 @@ - ${log.path}/sys-error.%d{yyyy-MM-dd}.log + ${log.path}/sys-error.%d{yyyy-MM-dd}.log.gz 60 diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index 474ce28e2..6ddaafeb9 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -14,7 +14,7 @@ - 5.4.1 + 5.5.0 diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java deleted file mode 100644 index cd01e33d5..000000000 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.dromara.common.core.config; - -import cn.hutool.core.util.ArrayUtil; -import org.dromara.common.core.exception.ServiceException; -import org.dromara.common.core.utils.SpringUtils; -import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.scheduling.annotation.AsyncConfigurer; - -import java.util.Arrays; -import java.util.concurrent.Executor; - -/** - * 异步配置 - *

- * 如果未使用虚拟线程则生效 - * - * @author Lion Li - */ -@AutoConfiguration -public class AsyncConfig implements AsyncConfigurer { - - /** - * 自定义 @Async 注解使用系统线程池 - */ - @Override - public Executor getAsyncExecutor() { - if(SpringUtils.isVirtual()) { - return new VirtualThreadTaskExecutor("async-"); - } - return SpringUtils.getBean("scheduledExecutorService"); - } - - /** - * 异步执行异常处理 - */ - @Override - public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { - return (throwable, method, objects) -> { - throwable.printStackTrace(); - StringBuilder sb = new StringBuilder(); - sb.append("Exception message - ").append(throwable.getMessage()) - .append(", Method name - ").append(method.getName()); - if (ArrayUtil.isNotEmpty(objects)) { - sb.append(", Parameter value - ").append(Arrays.toString(objects)); - } - throw new ServiceException(sb.toString()); - }; - } - -} diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java index 2630485a4..16f309d8f 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java @@ -7,11 +7,9 @@ import org.dromara.common.core.config.properties.ThreadPoolProperties; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.Threads; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -34,18 +32,6 @@ public class ThreadPoolConfig { private ScheduledExecutorService scheduledExecutorService; - @Bean(name = "threadPoolTaskExecutor") - @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true") - public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(core); - executor.setMaxPoolSize(core * 2); - executor.setQueueCapacity(threadPoolProperties.getQueueCapacity()); - executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds()); - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); - return executor; - } - /** * 执行周期性或定时任务 */ diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java index 273c7344a..0c6671a62 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java @@ -72,5 +72,10 @@ public interface Constants { */ Long TOP_PARENT_ID = 0L; + /** + * 加密头 + */ + String ENCRYPT_HEADER = "ENC_"; + } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java index 2e63f8aca..e011874a2 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java @@ -50,6 +50,11 @@ public class CompleteTaskDTO implements Serializable { */ private String notice; + /** + * 办理人(可不填 用于覆盖当前节点办理人) + */ + private String handler; + /** * 流程变量 */ diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java index 3934ada55..a6d4cb3a8 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java @@ -30,6 +30,11 @@ public class StartProcessDTO implements Serializable { */ private String flowCode; + /** + * 办理人(可不填 用于覆盖当前节点办理人) + */ + private String handler; + /** * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}} */ diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java index 85893e1dc..78fb40ec9 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java @@ -52,17 +52,17 @@ public class TaskAssigneeDTO implements Serializable { */ public static List convertToHandlerList( List sourceList, - Function storageId, + Function storageId, Function handlerCode, Function handlerName, - Function groupName, + Function groupName, Function createTimeMapper) { return sourceList.stream() .map(item -> new TaskHandler( - String.valueOf(storageId.apply(item)), + storageId.apply(item), handlerCode.apply(item), handlerName.apply(item), - groupName != null ? String.valueOf(groupName.apply(item)) : null, + groupName.apply(item), createTimeMapper.apply(item) )).collect(Collectors.toList()); } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java index 7b15b85ae..d830dbe03 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java @@ -27,6 +27,11 @@ public class ProcessEvent implements Serializable { */ private String flowCode; + /** + * 实例id + */ + private Long instanceId; + /** * 业务id */ diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java index a9bcf7e93..0984727ac 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java @@ -4,6 +4,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; +import java.util.Map; /** * 流程任务监听 @@ -46,6 +47,11 @@ public class ProcessTaskEvent implements Serializable { */ private Long taskId; + /** + * 实例id + */ + private Long instanceId; + /** * 业务id */ @@ -56,4 +62,9 @@ public class ProcessTaskEvent implements Serializable { */ private String status; + /** + * 办理参数 + */ + private Map params; + } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java index 36e33b24b..143c95907 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java @@ -26,6 +26,7 @@ public class PasswordLoginBody extends LoginBody { */ @NotBlank(message = "{user.password.not.blank}") @Length(min = 5, max = 30, message = "{user.password.length.valid}") +// @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}") private String password; } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java index cced26b2b..3f232492a 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java @@ -26,8 +26,12 @@ public class RegisterBody extends LoginBody { */ @NotBlank(message = "{user.password.not.blank}") @Length(min = 5, max = 30, message = "{user.password.length.valid}") +// @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}") private String password; + /** + * 用户类型 + */ private String userType; } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java index e9dc6ec9b..90f5752b1 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java @@ -1,11 +1,15 @@ package org.dromara.common.core.exception; -import lombok.*; +import cn.hutool.core.text.StrFormatter; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import java.io.Serial; /** - * 业务异常 + * 业务异常(支持占位符 {} ) * * @author ruoyi */ @@ -42,6 +46,10 @@ public final class ServiceException extends RuntimeException { this.code = code; } + public ServiceException(String message, Object... args) { + this.message = StrFormatter.format(message, args); + } + @Override public String getMessage() { return message; diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java index f93d1778a..725718ae8 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java @@ -3,6 +3,7 @@ package org.dromara.common.core.service; import org.dromara.common.core.domain.dto.DeptDTO; import java.util.List; +import java.util.Map; /** * 通用 部门服务 @@ -34,4 +35,12 @@ public interface DeptService { */ List selectDeptsByList(); + /** + * 根据部门 ID 列表查询部门名称映射关系 + * + * @param deptIds 部门 ID 列表 + * @return Map,其中 key 为部门 ID,value 为对应的部门名称 + */ + Map selectDeptNamesByIds(List deptIds); + } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java index 41d4e8308..58c68d676 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java @@ -1,5 +1,8 @@ package org.dromara.common.core.service; +import java.util.List; +import java.util.Map; + /** * 通用 岗位服务 * @@ -7,4 +10,12 @@ package org.dromara.common.core.service; */ public interface PostService { + /** + * 根据岗位 ID 列表查询岗位名称映射关系 + * + * @param postIds 岗位 ID 列表 + * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称 + */ + Map selectPostNamesByIds(List postIds); + } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java index ba62c82ae..d2805b7e0 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java @@ -1,5 +1,8 @@ package org.dromara.common.core.service; +import java.util.List; +import java.util.Map; + /** * 通用 角色服务 * @@ -7,4 +10,12 @@ package org.dromara.common.core.service; */ public interface RoleService { + /** + * 根据角色 ID 列表查询角色名称映射关系 + * + * @param roleIds 角色 ID 列表 + * @return Map,其中 key 为角色 ID,value 为对应的角色名称 + */ + Map selectRoleNamesByIds(List roleIds); + } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java index 4903c3860..eefeef011 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java @@ -100,28 +100,4 @@ public interface UserService { */ Map selectUserNamesByIds(List userIds); - /** - * 根据角色 ID 列表查询角色名称映射关系 - * - * @param roleIds 角色 ID 列表 - * @return Map,其中 key 为角色 ID,value 为对应的角色名称 - */ - Map selectRoleNamesByIds(List roleIds); - - /** - * 根据部门 ID 列表查询部门名称映射关系 - * - * @param deptIds 部门 ID 列表 - * @return Map,其中 key 为部门 ID,value 为对应的部门名称 - */ - Map selectDeptNamesByIds(List deptIds); - - /** - * 根据岗位 ID 列表查询岗位名称映射关系 - * - * @param postIds 岗位 ID 列表 - * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称 - */ - Map selectPostNamesByIds(List postIds); - } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java index 9d1a90195..706d357e7 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java @@ -82,6 +82,7 @@ public interface WorkflowService { * completeTask.getVariables().put("ignore", true); * * @param completeTask 参数 + * @return 结果 */ boolean completeTask(CompleteTaskDTO completeTask); @@ -90,6 +91,15 @@ public interface WorkflowService { * * @param taskId 任务ID * @param message 办理意见 + * @return 结果 */ boolean completeTask(Long taskId, String message); + + /** + * 启动流程并办理第一个任务 + * + * @param startProcess 参数 + * @return 结果 + */ + boolean startCompleteTask(StartProcessDTO startProcess); } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java index b52d95e16..b4d146242 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java @@ -293,7 +293,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils { // 校验时间跨度不超过最大限制 if (diff > maxValue) { - throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase()); + throw new ServiceException("最大时间跨度为 {} {}", maxValue, unit.toString().toLowerCase()); } } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java index bd1aab808..509026f72 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java @@ -115,7 +115,7 @@ public class ServletUtils extends JakartaServletUtil { public static Map getParamMap(ServletRequest request) { Map params = new HashMap<>(); for (Map.Entry entry : getParams(request).entrySet()) { - params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR)); + params.put(entry.getKey(), StringUtils.joinComma(entry.getValue())); } return params; } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java index 1342deb71..c5487c0b6 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; import java.util.*; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -31,8 +30,10 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return CollUtil.newArrayList(); } - // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 - return collection.stream().filter(function).collect(Collectors.toList()); + return collection.stream() + .filter(function) + // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 + .collect(Collectors.toList()); } /** @@ -40,13 +41,26 @@ public class StreamUtils { * * @param collection 需要查询的集合 * @param function 过滤方法 - * @return 找到符合条件的第一个元素,没有则返回null + * @return 找到符合条件的第一个元素,没有则返回 Optional.empty() */ - public static E findFirst(Collection collection, Predicate function) { + public static Optional findFirst(Collection collection, Predicate function) { if (CollUtil.isEmpty(collection)) { - return null; + return Optional.empty(); } - return collection.stream().filter(function).findFirst().orElse(null); + return collection.stream() + .filter(function) + .findFirst(); + } + + /** + * 找到流中满足条件的第一个元素值 + * + * @param collection 需要查询的集合 + * @param function 过滤方法 + * @return 找到符合条件的第一个元素,没有则返回 null + */ + public static E findFirstValue(Collection collection, Predicate function) { + return findFirst(collection,function).orElse(null); } /** @@ -54,13 +68,26 @@ public class StreamUtils { * * @param collection 需要查询的集合 * @param function 过滤方法 - * @return 找到符合条件的任意一个元素,没有则返回null + * @return 找到符合条件的任意一个元素,没有则返回 Optional.empty() */ public static Optional findAny(Collection collection, Predicate function) { if (CollUtil.isEmpty(collection)) { return Optional.empty(); } - return collection.stream().filter(function).findAny(); + return collection.stream() + .filter(function) + .findAny(); + } + + /** + * 找到流中任意一个满足条件的元素值 + * + * @param collection 需要查询的集合 + * @param function 过滤方法 + * @return 找到符合条件的任意一个元素,没有则返回null + */ + public static E findAnyValue(Collection collection, Predicate function) { + return findAny(collection,function).orElse(null); } /** @@ -86,7 +113,10 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return StringUtils.EMPTY; } - return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter)); + return collection.stream() + .map(function) + .filter(Objects::nonNull) + .collect(Collectors.joining(delimiter)); } /** @@ -100,8 +130,11 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return CollUtil.newArrayList(); } - // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 - return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList()); + return collection.stream() + .filter(Objects::nonNull) + .sorted(comparing) + // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 + .collect(Collectors.toList()); } /** @@ -118,7 +151,9 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return MapUtil.newHashMap(); } - return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l)); + return collection.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(key, Function.identity(), (l, r) -> l)); } /** @@ -137,7 +172,25 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return MapUtil.newHashMap(); } - return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l)); + return collection.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(key, value, (l, r) -> l)); + } + + /** + * 获取 map 中的数据作为新 Map 的 value ,key 不变 + * @param map 需要处理的map + * @param take 取值函数 + * @param map中的key类型 + * @param map中的value类型 + * @param 新map中的value类型 + * @return 新的map + */ + public static Map toMap(Map map, BiFunction take) { + if (CollUtil.isEmpty(map)) { + return MapUtil.newHashMap(); + } + return toMap(map.entrySet(), Map.Entry::getKey, entry -> take.apply(entry.getKey(), entry.getValue())); } /** @@ -154,8 +207,8 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return MapUtil.newHashMap(); } - return collection - .stream().filter(Objects::nonNull) + return collection.stream() + .filter(Objects::nonNull) .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList())); } @@ -175,8 +228,8 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return MapUtil.newHashMap(); } - return collection - .stream().filter(Objects::nonNull) + return collection.stream() + .filter(Objects::nonNull) .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList()))); } @@ -193,11 +246,11 @@ public class StreamUtils { * @return 分类后的map */ public static Map> group2Map(Collection collection, Function key1, Function key2) { - if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) { + if (CollUtil.isEmpty(collection)) { return MapUtil.newHashMap(); } - return collection - .stream().filter(Objects::nonNull) + return collection.stream() + .filter(Objects::nonNull) .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l))); } @@ -215,8 +268,7 @@ public class StreamUtils { if (CollUtil.isEmpty(collection)) { return CollUtil.newArrayList(); } - return collection - .stream() + return collection.stream() .map(function) .filter(Objects::nonNull) // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 @@ -234,11 +286,10 @@ public class StreamUtils { * @return 转化后的Set */ public static Set toSet(Collection collection, Function function) { - if (CollUtil.isEmpty(collection) || function == null) { + if (CollUtil.isEmpty(collection)) { return CollUtil.newHashSet(); } - return collection - .stream() + return collection.stream() .map(function) .filter(Objects::nonNull) .collect(Collectors.toSet()); @@ -258,26 +309,20 @@ public class StreamUtils { * @return 合并后的map */ public static Map merge(Map map1, Map map2, BiFunction merge) { - if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) { + if (CollUtil.isEmpty(map1) && CollUtil.isEmpty(map2)) { + // 如果两个 map 都为空,则直接返回空的 map return MapUtil.newHashMap(); - } else if (MapUtil.isEmpty(map1)) { - map1 = MapUtil.newHashMap(); - } else if (MapUtil.isEmpty(map2)) { - map2 = MapUtil.newHashMap(); + } else if (CollUtil.isEmpty(map1)) { + // 如果 map1 为空,则直接处理返回 map2 + return toMap(map2.entrySet(), Map.Entry::getKey, entry -> merge.apply(null, entry.getValue())); + } else if (CollUtil.isEmpty(map2)) { + // 如果 map2 为空,则直接处理返回 map1 + return toMap(map1.entrySet(), Map.Entry::getKey, entry -> merge.apply(entry.getValue(), null)); } - Set key = new HashSet<>(); - key.addAll(map1.keySet()); - key.addAll(map2.keySet()); - Map map = new HashMap<>(); - for (K t : key) { - X x = map1.get(t); - Y y = map2.get(t); - V z = merge.apply(x, y); - if (z != null) { - map.put(t, z); - } - } - return map; + Set keySet = new HashSet<>(); + keySet.addAll(map1.keySet()); + keySet.addAll(map2.keySet()); + return toMap(keySet, key -> key, key -> merge.apply(map1.get(key), map2.get(key))); } } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java index 716573431..6eac2fcbe 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java @@ -260,13 +260,13 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { if (s != null) { final int len = s.length(); if (s.length() <= size) { - sb.append(String.valueOf(c).repeat(size - len)); + sb.append(Convert.toStr(c).repeat(size - len)); sb.append(s); } else { return s.substring(len - size, len); } } else { - sb.append(String.valueOf(c).repeat(Math.max(0, size))); + sb.append(Convert.toStr(c).repeat(Math.max(0, size))); } return sb.toString(); } @@ -361,5 +361,24 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { return input; } } + /** + * 将可迭代对象中的元素使用逗号拼接成字符串 + * + * @param iterable 可迭代对象,如 List、Set 等 + * @return 拼接后的字符串 + */ + public static String joinComma(Iterable iterable) { + return StringUtils.join(iterable, SEPARATOR); + } + + /** + * 将数组中的元素使用逗号拼接成字符串 + * + * @param array 任意类型的数组 + * @return 拼接后的字符串 + */ + public static String joinComma(Object[] array) { + return StringUtils.join(array, SEPARATOR); + } } diff --git a/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 43c7fcfe0..5c3db08a5 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1,4 @@ org.dromara.common.core.config.ApplicationConfig -org.dromara.common.core.config.AsyncConfig org.dromara.common.core.config.ThreadPoolConfig org.dromara.common.core.config.ValidatorConfig org.dromara.common.core.utils.SpringUtils diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java index c199015c0..35b6ce9ea 100644 --- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java @@ -54,14 +54,15 @@ public class SpringDocConfig { openApi.externalDocs(properties.getExternalDocs()); openApi.tags(properties.getTags()); openApi.paths(properties.getPaths()); - openApi.components(properties.getComponents()); - Set keySet = properties.getComponents().getSecuritySchemes().keySet(); - List list = new ArrayList<>(); - SecurityRequirement securityRequirement = new SecurityRequirement(); - keySet.forEach(securityRequirement::addList); - list.add(securityRequirement); - openApi.security(list); - + if (properties.getComponents() != null) { + openApi.components(properties.getComponents()); + Set keySet = properties.getComponents().getSecuritySchemes().keySet(); + List list = new ArrayList<>(); + SecurityRequirement securityRequirement = new SecurityRequirement(); + keySet.forEach(securityRequirement::addList); + list.add(securityRequirement); + openApi.security(list); + } return openApi; } diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java index 098f6bc8d..38b22f388 100644 --- a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java +++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java @@ -6,6 +6,7 @@ import org.dromara.common.encrypt.properties.ApiDecryptProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistration; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -20,13 +21,14 @@ import org.springframework.context.annotation.Bean; public class ApiDecryptAutoConfiguration { @Bean - public FilterRegistrationBean cryptoFilterRegistration(ApiDecryptProperties properties) { - FilterRegistrationBean registration = new FilterRegistrationBean<>(); - registration.setDispatcherTypes(DispatcherType.REQUEST); - registration.setFilter(new CryptoFilter(properties)); - registration.addUrlPatterns("/*"); - registration.setName("cryptoFilter"); - registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); - return registration; + @FilterRegistration( + name = "cryptoFilter", + urlPatterns = "/*", + order = FilterRegistrationBean.HIGHEST_PRECEDENCE, + dispatcherTypes = DispatcherType.REQUEST + ) + public CryptoFilter cryptoFilter(ApiDecryptProperties properties) { + return new CryptoFilter(properties); } + } diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java index b5f194d76..5e2c7318e 100644 --- a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java +++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.ReflectUtil; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.io.Resources; +import org.dromara.common.core.constant.Constants; import org.dromara.common.core.utils.ObjectUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.encrypt.annotation.EncryptField; @@ -92,8 +93,12 @@ public class EncryptorManager { * @param encryptContext 加密相关的配置信息 */ public String encrypt(String value, EncryptContext encryptContext) { + if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) { + return value; + } IEncryptor encryptor = this.registAndGetEncryptor(encryptContext); - return encryptor.encrypt(value, encryptContext.getEncode()); + String encrypt = encryptor.encrypt(value, encryptContext.getEncode()); + return Constants.ENCRYPT_HEADER + encrypt; } /** @@ -103,8 +108,12 @@ public class EncryptorManager { * @param encryptContext 加密相关的配置信息 */ public String decrypt(String value, EncryptContext encryptContext) { + if (!StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) { + return value; + } IEncryptor encryptor = this.registAndGetEncryptor(encryptContext); - return encryptor.decrypt(value); + String str = StringUtils.removeStart(value, Constants.ENCRYPT_HEADER); + return encryptor.decrypt(str); } /** diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java new file mode 100644 index 000000000..204a88e25 --- /dev/null +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java @@ -0,0 +1,200 @@ +package org.dromara.common.excel.core; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.SneakyThrows; +import org.apache.poi.ss.util.CellRangeAddress; +import org.dromara.common.core.utils.reflect.ReflectUtils; +import org.dromara.common.excel.annotation.CellMerge; + +import java.lang.reflect.Field; +import java.util.*; + +/** + * 单元格合并处理器 + * + * @author Lion Li + */ +public class CellMergeHandler { + + private final boolean hasTitle; + private int rowIndex; + + private CellMergeHandler(final boolean hasTitle) { + this.hasTitle = hasTitle; + // 行合并开始下标 + this.rowIndex = hasTitle ? 1 : 0; + } + + @SneakyThrows + public List handle(List rows) { + // 如果入参为空集合则返回空集 + if (CollUtil.isEmpty(rows)) { + return Collections.emptyList(); + } + + // 获取有合并注解的字段 + Map mergeFields = getFieldColumnIndexMap(rows.get(0).getClass()); + // 如果没有需要合并的字段则返回空集 + if (CollUtil.isEmpty(mergeFields)) { + return Collections.emptyList(); + } + + // 结果集 + List result = new ArrayList<>(); + + // 生成两两合并单元格 + Map rowRepeatCellMap = new HashMap<>(); + for (Map.Entry item : mergeFields.entrySet()) { + Field field = item.getKey(); + FieldColumnIndex itemValue = item.getValue(); + int colNum = itemValue.colIndex(); + CellMerge cellMerge = itemValue.cellMerge(); + + for (int i = 0; i < rows.size(); i++) { + // 当前行数据 + Object currentRowObj = rows.get(i); + // 当前行数据字段值 + Object currentRowObjFieldVal = ReflectUtils.invokeGetter(currentRowObj, field.getName()); + + // 空值跳过不处理 + if (currentRowObjFieldVal == null || "".equals(currentRowObjFieldVal)) { + continue; + } + + // 单元格合并Map是否存在数据,如果不存在则添加当前行的字段值 + if (!rowRepeatCellMap.containsKey(field)) { + rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i)); + continue; + } + + // 获取 单元格合并Map 中字段值 + RepeatCell repeatCell = rowRepeatCellMap.get(field); + Object cellValue = repeatCell.value(); + int current = repeatCell.current(); + + // 检查是否满足合并条件 + // currentRowObj 当前行数据 + // rows.get(i - 1) 上一行数据 注:由于 if (!rowRepeatCellMap.containsKey(field)) 条件的存在,所以该 i 必不可能小于1 + // cellMerge 当前行字段合并注解 + boolean merge = isMerge(currentRowObj, rows.get(i - 1), cellMerge); + + // 是否添加到结果集 + boolean isAddResult = false; + // 最新行 + int lastRow = i + rowIndex - 1; + + // 如果当前行字段值和缓存中的字段值不相等,或不满足合并条件,则替换 + if (!currentRowObjFieldVal.equals(cellValue) || !merge) { + rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i)); + isAddResult = true; + } + + // 如果最后一行不能合并,检查之前的数据是否需要合并;如果最后一行可以合并,则直接合并到最后 + if (i == rows.size() - 1) { + isAddResult = true; + if (i > current) { + lastRow = i + rowIndex; + } + } + + if (isAddResult && i > current) { + result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum)); + } + } + } + return result; + } + + /** + * 获取带有合并注解的字段列索引和合并注解信息Map集 + */ + private Map getFieldColumnIndexMap(Class clazz) { + boolean annotationPresent = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class); + Field[] fields = ReflectUtils.getFields(clazz, field -> { + if ("serialVersionUID".equals(field.getName())) { + return false; + } + if (field.isAnnotationPresent(ExcelIgnore.class)) { + return false; + } + return !annotationPresent || field.isAnnotationPresent(ExcelProperty.class); + }); + + // 有注解的字段 + Map mergeFields = new HashMap<>(); + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + if (!field.isAnnotationPresent(CellMerge.class)) { + continue; + } + CellMerge cm = field.getAnnotation(CellMerge.class); + int index = cm.index() == -1 ? i : cm.index(); + mergeFields.put(field, FieldColumnIndex.of(index, cm)); + + if (hasTitle) { + ExcelProperty property = field.getAnnotation(ExcelProperty.class); + rowIndex = Math.max(rowIndex, property.value().length); + } + } + return mergeFields; + } + + private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) { + final String[] mergeBy = cellMerge.mergeBy(); + if (StrUtil.isAllNotBlank(mergeBy)) { + //比对当前行和上一行的各个属性值一一比对 如果全为真 则为真 + for (String fieldName : mergeBy) { + final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName); + final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName); + if (!Objects.equals(valPre, valCurrent)) { + //依赖字段如有任一不等值,则标记为不可合并 + return false; + } + } + } + return true; + } + + /** + * 单元格合并 + */ + record RepeatCell(Object value, int current) { + static RepeatCell of(Object value, int current) { + return new RepeatCell(value, current); + } + } + + /** + * 字段列索引和合并注解信息 + */ + record FieldColumnIndex(int colIndex, CellMerge cellMerge) { + static FieldColumnIndex of(int colIndex, CellMerge cellMerge) { + return new FieldColumnIndex(colIndex, cellMerge); + } + } + + /** + * 创建一个单元格合并处理器实例 + * + * @param hasTitle 是否合并标题 + * @return 单元格合并处理器 + */ + public static CellMergeHandler of(final boolean hasTitle) { + return new CellMergeHandler(hasTitle); + } + + /** + * 创建一个单元格合并处理器实例(默认不合并标题) + * + * @return 单元格合并处理器 + */ + public static CellMergeHandler of() { + return new CellMergeHandler(false); + } + +} diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java index 515f68e1b..34bf4a42f 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java @@ -1,24 +1,15 @@ package org.dromara.common.excel.core; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.hutool.core.util.StrUtil; -import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.metadata.Head; import cn.idev.excel.write.handler.WorkbookWriteHandler; import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext; import cn.idev.excel.write.merge.AbstractMergeStrategy; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.util.CellRangeAddress; -import org.dromara.common.core.utils.reflect.ReflectUtils; -import org.dromara.common.excel.annotation.CellMerge; -import java.lang.reflect.Field; import java.util.*; /** @@ -30,134 +21,39 @@ import java.util.*; public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler { private final List cellList; - private final boolean hasTitle; - private int rowIndex; + + public CellMergeStrategy(List cellList) { + this.cellList = cellList; + } public CellMergeStrategy(List list, boolean hasTitle) { - this.hasTitle = hasTitle; - // 行合并开始下标 - this.rowIndex = hasTitle ? 1 : 0; - this.cellList = handle(list, hasTitle); + this.cellList = CellMergeHandler.of(hasTitle).handle(list); } @Override protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { + if (CollUtil.isEmpty(cellList)){ + return; + } //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空 final int rowIndex = cell.getRowIndex(); - if (CollUtil.isNotEmpty(cellList)){ - for (CellRangeAddress cellAddresses : cellList) { - final int firstRow = cellAddresses.getFirstRow(); - if (cellAddresses.isInRange(cell) && rowIndex != firstRow){ - cell.setBlank(); - } + for (CellRangeAddress cellAddresses : cellList) { + final int firstRow = cellAddresses.getFirstRow(); + if (cellAddresses.isInRange(cell) && rowIndex != firstRow){ + cell.setBlank(); } } } @Override public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) { + if (CollUtil.isEmpty(cellList)){ + return; + } //当前表格写完后,统一写入 - if (CollUtil.isNotEmpty(cellList)){ - for (CellRangeAddress item : cellList) { - context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item); - } + for (CellRangeAddress item : cellList) { + context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item); } } - @SneakyThrows - private List handle(List list, boolean hasTitle) { - List cellList = new ArrayList<>(); - if (CollUtil.isEmpty(list)) { - return cellList; - } - Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName())); - - // 有注解的字段 - List mergeFields = new ArrayList<>(); - List mergeFieldsIndex = new ArrayList<>(); - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field.isAnnotationPresent(CellMerge.class)) { - CellMerge cm = field.getAnnotation(CellMerge.class); - mergeFields.add(field); - mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index()); - if (hasTitle) { - ExcelProperty property = field.getAnnotation(ExcelProperty.class); - rowIndex = Math.max(rowIndex, property.value().length); - } - } - } - - Map map = new HashMap<>(); - // 生成两两合并单元格 - for (int i = 0; i < list.size(); i++) { - for (int j = 0; j < mergeFields.size(); j++) { - Field field = mergeFields.get(j); - Object val = ReflectUtils.invokeGetter(list.get(i), field.getName()); - - int colNum = mergeFieldsIndex.get(j); - if (!map.containsKey(field)) { - map.put(field, new RepeatCell(val, i)); - } else { - RepeatCell repeatCell = map.get(field); - Object cellValue = repeatCell.getValue(); - if (cellValue == null || "".equals(cellValue)) { - // 空值跳过不合并 - continue; - } - - if (!cellValue.equals(val)) { - if ((i - repeatCell.getCurrent() > 1)) { - cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); - } - map.put(field, new RepeatCell(val, i)); - } else if (i == list.size() - 1) { - if (!isMerge(list, i, field)) { - // 如果最后一行不能合并,检查之前的数据是否需要合并 - if (i - repeatCell.getCurrent() > 1) { - cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); - } - } else if (i > repeatCell.getCurrent()) { - // 如果最后一行可以合并,则直接合并到最后 - cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum)); - } - } else if (!isMerge(list, i, field)) { - if ((i - repeatCell.getCurrent() > 1)) { - cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); - } - map.put(field, new RepeatCell(val, i)); - } - } - } - } - return cellList; - } - - private boolean isMerge(List list, int i, Field field) { - boolean isMerge = true; - CellMerge cm = field.getAnnotation(CellMerge.class); - final String[] mergeBy = cm.mergeBy(); - if (StrUtil.isAllNotBlank(mergeBy)) { - //比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真 - for (String fieldName : mergeBy) { - final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName); - final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName); - if (!Objects.equals(valPre, valCurrent)) { - //依赖字段如有任一不等值,则标记为不可合并 - isMerge = false; - } - } - } - return isMerge; - } - - @Data - @AllArgsConstructor - static class RepeatCell { - - private Object value; - - private int current; - - } } diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java index 8b53a0ce0..7cdb5c5c0 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java @@ -1,5 +1,6 @@ package org.dromara.common.excel.core; +import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import lombok.AllArgsConstructor; import lombok.Data; @@ -65,7 +66,7 @@ public class DropDownOptions { StringBuilder stringBuffer = new StringBuilder(); String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$"; for (int i = 0; i < vars.length; i++) { - String var = StrUtil.trimToEmpty(String.valueOf(vars[i])); + String var = StrUtil.trimToEmpty(Convert.toStr(vars[i])); if (!var.matches(regex)) { throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字"); } diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java index f3b641545..139728390 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java @@ -1,6 +1,7 @@ package org.dromara.common.excel.core; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.ObjectUtil; @@ -103,7 +104,7 @@ public class ExcelDownHandler implements SheetWriteHandler { if (StringUtils.isNotBlank(dictType)) { // 如果传递了字典名,则依据字典建立下拉 Collection values = Optional.ofNullable(dictService.getAllDictByDictType(dictType)) - .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType))) + .orElseThrow(() -> new ServiceException("字典 {} 不存在", dictType)) .values(); options = new ArrayList<>(values); } else if (StringUtils.isNotBlank(converterExp)) { @@ -115,7 +116,7 @@ public class ExcelDownHandler implements SheetWriteHandler { // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑 ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class); List values = EnumUtil.getFieldValues(format.enumClass(), format.textField()); - options = StreamUtils.toList(values, String::valueOf); + options = StreamUtils.toList(values, Convert::toStr); } if (ObjectUtil.isNotEmpty(options)) { // 仅当下拉可选项不为空时执行 diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java index 1b35e559a..74dbccbac 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java @@ -27,6 +27,7 @@ import java.io.UnsupportedEncodingException; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * Excel相关处理 @@ -203,6 +204,44 @@ public class ExcelUtil { builder.doWrite(list); } + /** + * 导出excel + * + * @param headType 带Excel注解的类型 + * @param os 输出流 + * @param options Excel下拉可选项 + * @param consumer 导出助手消费函数 + */ + public static void exportExcel(Class headType, OutputStream os, List options, Consumer> consumer) { + try (ExcelWriter writer = FastExcel.write(os, headType) + .autoCloseStream(false) + // 自动适配 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + // 大数值自动转换 防止失真 + .registerConverter(new ExcelBigNumberConvert()) + // 批注必填项处理 + .registerWriteHandler(new DataWriteHandler(headType)) + // 添加下拉框操作 + .registerWriteHandler(new ExcelDownHandler(options)) + .build()) { + // 执行消费函数 + consumer.accept(ExcelWriterWrapper.of(writer)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 导出excel + * + * @param headType 带Excel注解的类型 + * @param os 输出流 + * @param consumer 导出助手消费函数 + */ + public static void exportExcel(Class headType, OutputStream os, Consumer> consumer) { + exportExcel(headType, os, null, consumer); + } + /** * 单表多数据模板导出 模板格式为 {.属性} * diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelWriterWrapper.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelWriterWrapper.java new file mode 100644 index 000000000..396f3713e --- /dev/null +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelWriterWrapper.java @@ -0,0 +1,127 @@ +package org.dromara.common.excel.utils; + +import cn.idev.excel.ExcelWriter; +import cn.idev.excel.FastExcel; +import cn.idev.excel.context.WriteContext; +import cn.idev.excel.write.builder.ExcelWriterSheetBuilder; +import cn.idev.excel.write.builder.ExcelWriterTableBuilder; +import cn.idev.excel.write.metadata.WriteSheet; +import cn.idev.excel.write.metadata.WriteTable; +import cn.idev.excel.write.metadata.fill.FillConfig; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * ExcelWriterWrapper Excel写出包装器 + *
+ * 提供了一组与 ExcelWriter 一一对应的写出方法,避免直接提供 ExcelWriter 而导致的一些不可控问题(比如提前关闭了IO流等) + * + * @author 秋辞未寒 + * @see ExcelWriter + */ +public record ExcelWriterWrapper(ExcelWriter excelWriter) { + + public void write(Collection data, WriteSheet writeSheet) { + excelWriter.write(data, writeSheet); + } + + public void write(Supplier> supplier, WriteSheet writeSheet) { + excelWriter.write(supplier.get(), writeSheet); + } + + public void write(Collection data, WriteSheet writeSheet, WriteTable writeTable) { + excelWriter.write(data, writeSheet, writeTable); + } + + public void write(Supplier> supplier, WriteSheet writeSheet, WriteTable writeTable) { + excelWriter.write(supplier.get(), writeSheet, writeTable); + } + + public void fill(Object data, WriteSheet writeSheet) { + excelWriter.fill(data, writeSheet); + } + + public void fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) { + excelWriter.fill(data, fillConfig, writeSheet); + } + + public void fill(Supplier supplier, WriteSheet writeSheet) { + excelWriter.fill(supplier, writeSheet); + } + + public void fill(Supplier supplier, FillConfig fillConfig, WriteSheet writeSheet) { + excelWriter.fill(supplier, fillConfig, writeSheet); + } + + public WriteContext writeContext() { + return excelWriter.writeContext(); + } + + /** + * 创建一个 ExcelWriterWrapper + * + * @param excelWriter ExcelWriter + * @return ExcelWriterWrapper + */ + public static ExcelWriterWrapper of(ExcelWriter excelWriter) { + return new ExcelWriterWrapper<>(excelWriter); + } + + // -------------------------------- sheet start + + public static WriteSheet buildSheet(Integer sheetNo, String sheetName) { + return sheetBuilder(sheetNo, sheetName).build(); + } + + public static WriteSheet buildSheet(Integer sheetNo) { + return sheetBuilder(sheetNo).build(); + } + + public static WriteSheet buildSheet(String sheetName) { + return sheetBuilder(sheetName).build(); + } + + public static WriteSheet buildSheet() { + return sheetBuilder().build(); + } + + public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo, String sheetName) { + return FastExcel.writerSheet(sheetNo, sheetName); + } + + public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo) { + return FastExcel.writerSheet(sheetNo); + } + + public static ExcelWriterSheetBuilder sheetBuilder(String sheetName) { + return FastExcel.writerSheet(sheetName); + } + + public static ExcelWriterSheetBuilder sheetBuilder() { + return FastExcel.writerSheet(); + } + + // -------------------------------- sheet end + + // -------------------------------- table start + + public static WriteTable buildTable(Integer tableNo) { + return tableBuilder(tableNo).build(); + } + + public static WriteTable buildTable() { + return tableBuilder().build(); + } + + public static ExcelWriterTableBuilder tableBuilder(Integer tableNo) { + return FastExcel.writerTable(tableNo); + } + + public static ExcelWriterTableBuilder tableBuilder() { + return FastExcel.writerTable(); + } + + // -------------------------------- table end + +} diff --git a/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java index 77cf83381..039257387 100644 --- a/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java +++ b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java @@ -1,5 +1,6 @@ package org.dromara.common.json.config; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; @@ -28,20 +29,24 @@ import java.util.TimeZone; @AutoConfiguration(before = JacksonAutoConfiguration.class) public class JacksonConfig { + @Bean + public Module registerJavaTimeModule() { + // 全局配置序列化返回 JSON 处理 + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE); + javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE); + javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE); + javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); + javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer()); + return javaTimeModule; + } + @Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder -> { - // 全局配置序列化返回 JSON 处理 - JavaTimeModule javaTimeModule = new JavaTimeModule(); - javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE); - javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE); - javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE); - javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); - javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); - javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer()); - builder.modules(javaTimeModule); builder.timeZone(TimeZone.getDefault()); log.info("初始化 jackson 配置"); }; diff --git a/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/CustomDateDeserializer.java b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/CustomDateDeserializer.java index 069b924f0..21c6a6aa6 100644 --- a/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/CustomDateDeserializer.java +++ b/ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/CustomDateDeserializer.java @@ -1,9 +1,11 @@ package org.dromara.common.json.handler; +import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; +import org.dromara.common.core.utils.ObjectUtils; import java.io.IOException; import java.util.Date; @@ -25,7 +27,11 @@ public class CustomDateDeserializer extends JsonDeserializer { */ @Override public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return DateUtil.parse(p.getText()); + DateTime parse = DateUtil.parse(p.getText()); + if (ObjectUtils.isNull(parse)) { + return null; + } + return parse.toJdkDate(); } } diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java index 8ab2719e1..d68be2251 100644 --- a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java +++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java @@ -27,9 +27,7 @@ import org.springframework.http.HttpMethod; import org.springframework.validation.BindingResult; import org.springframework.web.multipart.MultipartFile; -import java.util.Collection; -import java.util.Map; -import java.util.StringJoiner; +import java.util.*; /** * 操作日志记录处理 @@ -176,14 +174,28 @@ public class LogAspect { if (ArrayUtil.isEmpty(paramsArray)) { return params.toString(); } + String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES); for (Object o : paramsArray) { if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) { - String str = JsonUtils.toJsonString(o); - Dict dict = JsonUtils.parseMap(str); - if (MapUtil.isNotEmpty(dict)) { - MapUtil.removeAny(dict, EXCLUDE_PROPERTIES); - MapUtil.removeAny(dict, excludeParamNames); - str = JsonUtils.toJsonString(dict); + String str = ""; + if (o instanceof List list) { + List list1 = new ArrayList<>(); + for (Object obj : list) { + String str1 = JsonUtils.toJsonString(obj); + Dict dict = JsonUtils.parseMap(str1); + if (MapUtil.isNotEmpty(dict)) { + MapUtil.removeAny(dict, exclude); + list1.add(dict); + } + } + str = JsonUtils.toJsonString(list1); + } else { + str = JsonUtils.toJsonString(o); + Dict dict = JsonUtils.parseMap(str); + if (MapUtil.isNotEmpty(dict)) { + MapUtil.removeAny(dict, exclude); + str = JsonUtils.toJsonString(dict); + } } params.add(str); } diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAdvice.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAdvice.java new file mode 100644 index 000000000..54d5ad41a --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAdvice.java @@ -0,0 +1,54 @@ +package org.dromara.common.mybatis.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.dromara.common.mybatis.annotation.DataPermission; +import org.dromara.common.mybatis.helper.DataPermissionHelper; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * 数据权限注解Advice + * + * @author 秋辞未寒 + */ +@Slf4j +public class DataPermissionAdvice implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + Object target = invocation.getThis(); + Method method = invocation.getMethod(); + Object[] args = invocation.getArguments(); + // 设置权限注解 + DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method, args)); + try { + // 执行代理方法 + return invocation.proceed(); + } finally { + // 清除权限注解 + DataPermissionHelper.removePermission(); + } + } + + /** + * 获取数据权限注解 + */ + private DataPermission getDataPermissionAnnotation(Object target, Method method,Object[] args){ + DataPermission dataPermission = method.getAnnotation(DataPermission.class); + // 优先获取方法上的注解 + if (dataPermission != null) { + return dataPermission; + } + // 方法上没有注解,则获取类上的注解 + Class targetClass = target.getClass(); + // 如果是 JDK 动态代理,则获取真实的Class实例 + if (Proxy.isProxyClass(targetClass)) { + targetClass = targetClass.getInterfaces()[0]; + } + dataPermission = targetClass.getAnnotation(DataPermission.class); + return dataPermission; + } +} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAspect.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAspect.java deleted file mode 100644 index 1c83cc392..000000000 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAspect.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.dromara.common.mybatis.aspect; - -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.AfterReturning; -import org.aspectj.lang.annotation.AfterThrowing; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.dromara.common.mybatis.annotation.DataPermission; -import org.dromara.common.mybatis.helper.DataPermissionHelper; - -/** - * 数据权限处理 - * - * @author Lion Li - */ -@Slf4j -@Aspect -public class DataPermissionAspect { - - /** - * 处理请求前执行 - */ - @Before(value = "@annotation(dataPermission)") - public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) { - DataPermissionHelper.setPermission(dataPermission); - } - - /** - * 处理完请求后执行 - * - * @param joinPoint 切点 - */ - @AfterReturning(pointcut = "@annotation(dataPermission)") - public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) { - DataPermissionHelper.removePermission(); - } - - /** - * 拦截异常操作 - * - * @param joinPoint 切点 - * @param e 异常 - */ - @AfterThrowing(value = "@annotation(dataPermission)", throwing = "e") - public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) { - DataPermissionHelper.removePermission(); - } - -} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcut.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcut.java new file mode 100644 index 000000000..4b7d945a3 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcut.java @@ -0,0 +1,39 @@ +package org.dromara.common.mybatis.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.mybatis.annotation.DataPermission; +import org.springframework.aop.support.StaticMethodMatcherPointcut; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * 数据权限匹配切点 + * + * @author 秋辞未寒 + */ +@Slf4j +@SuppressWarnings("all") +public class DataPermissionPointcut extends StaticMethodMatcherPointcut { + + @Override + public boolean matches(Method method, Class targetClass) { + // 优先匹配方法 + // 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口 + if (method.isAnnotationPresent(DataPermission.class)) { + return true; + } + + // MyBatis 的 Mapper 就是通过 JDK 动态代理实现的,所以这里需要检查是否匹配 JDK 的动态代理 + Class targetClassRef = targetClass; + if (Proxy.isProxyClass(targetClassRef)) { + // 数据权限注解不对继承生效,但由于 SpringIOC 容器拿到的实际上是 MyBatis 代理过后的 Mapper,而 targetClass.isAnnotationPresent 实际匹配的是 Proxy 类的注解,不会查找代理类。 + // 所以这里不能用 targetClass.isAnnotationPresent,只能用 AnnotatedElementUtils.hasAnnotation 或 targetClass.getInterfaces()[0].isAnnotationPresent 去做匹配,以检查被代理的 MapperClass 是否具有注解 + // 原理:JDK 动态代理本质上就是对接口进行实现然后对具体的接口实现做代理,所以直接通过接口可以拿到实际的 MapperClass + targetClassRef = targetClass.getInterfaces()[0]; + + } + return targetClassRef.isAnnotationPresent(DataPermission.class); + } + +} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcutAdvisor.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcutAdvisor.java new file mode 100644 index 000000000..351288ce4 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcutAdvisor.java @@ -0,0 +1,33 @@ +package org.dromara.common.mybatis.aspect; + +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; + +/** + * 数据权限注解切面定义 + * + * @author 秋辞未寒 + */ +@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(); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this.advice; + } + +} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java index 00c26912e..b08825731 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java @@ -11,15 +11,17 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import org.dromara.common.core.factory.YmlPropertySourceFactory; import org.dromara.common.core.utils.SpringUtils; -import org.dromara.common.mybatis.aspect.DataPermissionAspect; +import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor; import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler; import org.dromara.common.mybatis.handler.MybatisExceptionHandler; import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler; import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; +import org.springframework.context.annotation.Role; import org.springframework.transaction.annotation.EnableTransactionManagement; /** @@ -27,6 +29,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; * * @author Lion Li */ +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @EnableTransactionManagement(proxyTargetClass = true) @MapperScan("${mybatis-plus.mapperPackage}") @PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class) @@ -54,15 +57,16 @@ public class MybatisPlusConfig { * 数据权限拦截器 */ public PlusDataPermissionInterceptor dataPermissionInterceptor() { - return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage")); + return new PlusDataPermissionInterceptor(); } /** * 数据权限切面处理器 */ @Bean - public DataPermissionAspect dataPermissionAspect() { - return new DataPermissionAspect(); + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public DataPermissionPointcutAdvisor dataPermissionPointcutAdvisor() { + return new DataPermissionPointcutAdvisor(); } /** diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java index 5084424eb..2d5244bcf 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java @@ -42,17 +42,46 @@ public enum DataBaseType { * 根据数据库产品名称查找对应的数据库类型 * * @param databaseProductName 数据库产品名称 - * @return 对应的数据库类型枚举值,如果未找到则返回 null + * @return 对应的数据库类型枚举值 */ public static DataBaseType find(String databaseProductName) { if (StringUtils.isBlank(databaseProductName)) { - return null; + return MY_SQL; } for (DataBaseType type : values()) { if (type.getType().equals(databaseProductName)) { return type; } } - return null; + return MY_SQL; } + + /** + * 判断是否为 MySQL 类型 + */ + public boolean isMySql() { + return this == MY_SQL; + } + + /** + * 判断是否为 Oracle 类型 + */ + public boolean isOracle() { + return this == ORACLE; + } + + /** + * 判断是否为 PostgreSQL 类型 + */ + public boolean isPostgreSql() { + return this == POSTGRE_SQL; + } + + /** + * 判断是否为 SQL Server 类型 + */ + public boolean isSqlServer() { + return this == SQL_SERVER; + } + } diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java index 6dee1214a..5c2ca983b 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java @@ -1,6 +1,5 @@ package org.dromara.common.mybatis.handler; -import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import lombok.AllArgsConstructor; @@ -10,7 +9,6 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; -import org.apache.ibatis.io.Resources; import org.dromara.common.core.domain.dto.RoleDTO; import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.exception.ServiceException; @@ -22,22 +20,13 @@ import org.dromara.common.mybatis.annotation.DataPermission; import org.dromara.common.mybatis.enums.DataScopeType; import org.dromara.common.mybatis.helper.DataPermissionHelper; import org.dromara.common.satoken.utils.LoginHelper; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.expression.BeanFactoryResolver; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.core.type.ClassMetadata; -import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.expression.*; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.util.ClassUtils; -import java.lang.reflect.Method; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; /** @@ -49,11 +38,6 @@ import java.util.function.Function; @Slf4j public class PlusDataPermissionHandler { - /** - * 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行) - */ - private final Map dataPermissionCacheMap = new ConcurrentHashMap<>(); - /** * spel 解析器 */ @@ -64,27 +48,17 @@ public class PlusDataPermissionHandler { */ private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory()); - /** - * 构造方法,扫描指定包下的 Mapper 类并初始化缓存 - * - * @param mapperPackage Mapper 类所在的包路径 - */ - public PlusDataPermissionHandler(String mapperPackage) { - scanMapperClasses(mapperPackage); - } - /** * 获取数据过滤条件的 SQL 片段 * * @param where 原始的查询条件表达式 - * @param mappedStatementId Mapper 方法的 ID * @param isSelect 是否为查询语句 * @return 数据过滤条件的 SQL 片段 */ - public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) { + public Expression getSqlSegment(Expression where, boolean isSelect) { try { // 获取数据权限配置 - DataPermission dataPermission = getDataPermission(mappedStatementId); + DataPermission dataPermission = getDataPermission(); // 获取当前登录用户信息 LoginUser currentUser = DataPermissionHelper.getVariable("user"); if (ObjectUtil.isNull(currentUser)) { @@ -206,92 +180,22 @@ public class PlusDataPermissionHandler { return StringUtils.EMPTY; } - /** - * 扫描指定包下的 Mapper 类,并查找其中带有特定注解的方法或类 - * - * @param mapperPackage Mapper 类所在的包路径 - */ - private void scanMapperClasses(String mapperPackage) { - // 创建资源解析器和元数据读取工厂 - PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); - CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(); - // 将 Mapper 包路径按分隔符拆分为数组 - String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); - String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX; - try { - for (String packagePattern : packagePatternArray) { - // 将包路径转换为资源路径 - String path = ClassUtils.convertClassNameToResourcePath(packagePattern); - // 获取指定路径下的所有 .class 文件资源 - Resource[] resources = resolver.getResources(classpath + path + "/*.class"); - for (Resource resource : resources) { - // 获取资源的类元数据 - ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata(); - // 获取资源对应的类对象 - Class clazz = Resources.classForName(classMetadata.getClassName()); - // 查找类中的特定注解 - findAnnotation(clazz); - } - } - } catch (Exception e) { - log.error("初始化数据安全缓存时出错:{}", e.getMessage()); - } - } - - /** - * 在指定的类中查找特定的注解 DataPermission,并将带有这个注解的方法或类存储到 dataPermissionCacheMap 中 - * - * @param clazz 要查找的类 - */ - private void findAnnotation(Class clazz) { - DataPermission dataPermission; - for (Method method : clazz.getMethods()) { - if (method.isDefault() || method.isVarArgs()) { - continue; - } - String mappedStatementId = clazz.getName() + "." + method.getName(); - if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) { - dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class); - dataPermissionCacheMap.put(mappedStatementId, dataPermission); - } - } - if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) { - dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class); - dataPermissionCacheMap.put(clazz.getName(), dataPermission); - } - } - /** * 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象 * - * @param mapperId 映射语句 ID * @return DataPermission 注解对象,如果不存在则返回 null */ - public DataPermission getDataPermission(String mapperId) { - // 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象 - if (DataPermissionHelper.getPermission() != null) { - return DataPermissionHelper.getPermission(); - } - // 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象 - if (dataPermissionCacheMap.containsKey(mapperId)) { - return dataPermissionCacheMap.get(mapperId); - } - // 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找 - String clazzName = mapperId.substring(0, mapperId.lastIndexOf(".")); - if (dataPermissionCacheMap.containsKey(clazzName)) { - return dataPermissionCacheMap.get(clazzName); - } - return null; + public DataPermission getDataPermission() { + return DataPermissionHelper.getPermission(); } /** * 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象 * - * @param mapperId 映射语句 ID * @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true */ - public boolean invalid(String mapperId) { - return getDataPermission(mapperId) == null; + public boolean invalid() { + return getDataPermission() == null; } /** diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java index cd43c6831..487ffd375 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java @@ -26,7 +26,14 @@ public class DataBaseHelper { private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class); /** - * 获取当前数据库类型 + * 获取当前数据源对应的数据库类型 + *

+ * 通过 DynamicRoutingDataSource 获取当前线程绑定的数据源, + * 然后从数据源获取数据库连接,利用连接的元数据获取数据库产品名称, + * 最后调用 DataBaseType.find 方法将数据库名称转换为对应的枚举类型 + * + * @return 当前数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL + * @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常 */ public static DataBaseType getDataBaseType() { DataSource dataSource = DS.determineDataSource(); @@ -39,37 +46,31 @@ public class DataBaseHelper { } } - public static boolean isMySql() { - return DataBaseType.MY_SQL == getDataBaseType(); - } - - public static boolean isOracle() { - return DataBaseType.ORACLE == getDataBaseType(); - } - - public static boolean isPostgerSql() { - return DataBaseType.POSTGRE_SQL == getDataBaseType(); - } - - public static boolean isSqlServer() { - return DataBaseType.SQL_SERVER == getDataBaseType(); - } - + /** + * 根据当前数据库类型,生成兼容的 FIND_IN_SET 语句片段 + *

+ * 用于判断指定值是否存在于逗号分隔的字符串列中,SQL写法根据不同数据库方言自动切换: + * - Oracle 使用 instr 函数 + * - PostgreSQL 使用 strpos 函数 + * - SQL Server 使用 charindex 函数 + * - 其他默认使用 MySQL 的 find_in_set 函数 + * + * @param var1 要查找的值(支持任意类型,内部会转换成字符串) + * @param var2 存储逗号分隔值的数据库列名 + * @return 适用于当前数据库的 SQL 条件字符串,通常用于 where 或 apply 中拼接 + */ public static String findInSet(Object var1, String var2) { - DataBaseType dataBasyType = getDataBaseType(); String var = Convert.toStr(var1); - if (dataBasyType == DataBaseType.SQL_SERVER) { - // charindex(',100,' , ',0,100,101,') <> 0 - return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2); - } else if (dataBasyType == DataBaseType.POSTGRE_SQL) { - // (select strpos(',0,100,101,' , ',100,')) <> 0 - return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var); - } else if (dataBasyType == DataBaseType.ORACLE) { + return switch (getDataBaseType()) { // instr(',0,100,101,' , ',100,') <> 0 - return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var); - } - // find_in_set(100 , '0,100,101') - return "find_in_set('%s' , %s) <> 0".formatted(var, var2); + case ORACLE -> "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var); + // (select strpos(',0,100,101,' , ',100,')) <> 0 + case POSTGRE_SQL -> "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var); + // charindex(',100,' , ',0,100,101,') <> 0 + case SQL_SERVER -> "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2); + // find_in_set(100 , '0,100,101') + default -> "find_in_set('%s' , %s) <> 0".formatted(var, var2); + }; } /** diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java index 85a4d0abc..b37d96ed1 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java @@ -35,16 +35,7 @@ import java.util.List; @Slf4j public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor { - private final PlusDataPermissionHandler dataPermissionHandler; - - /** - * 构造函数,初始化 PlusDataPermissionHandler 实例 - * - * @param mapperPackage 扫描的映射器包 - */ - public PlusDataPermissionInterceptor(String mapperPackage) { - this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage); - } + private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler(); /** * 在执行查询之前,检查并处理数据权限相关逻辑 @@ -64,7 +55,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto return; } // 检查是否缺少有效的数据权限注解 - if (dataPermissionHandler.invalid(ms.getId())) { + if (dataPermissionHandler.invalid()) { return; } // 解析 sql 分配对应方法 @@ -92,7 +83,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto return; } // 检查是否缺少有效的数据权限注解 - if (dataPermissionHandler.invalid(ms.getId())) { + if (dataPermissionHandler.invalid()) { return; } PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); @@ -128,7 +119,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto */ @Override protected void processUpdate(Update update, int index, String sql, Object obj) { - Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false); + Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), false); if (null != sqlSegment) { update.setWhere(sqlSegment); } @@ -144,7 +135,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto */ @Override protected void processDelete(Delete delete, int index, String sql, Object obj) { - Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false); + Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), false); if (null != sqlSegment) { delete.setWhere(sqlSegment); } @@ -157,7 +148,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto * @param mappedStatementId 映射语句的 ID */ protected void setWhere(PlainSelect plainSelect, String mappedStatementId) { - Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true); + Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), true); if (null != sqlSegment) { plainSelect.setWhere(sqlSegment); } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java index de5119e91..b9a90dc75 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java @@ -2,6 +2,7 @@ package org.dromara.common.oss.core; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.IdUtil; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.Constants; import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.StringUtils; @@ -13,9 +14,7 @@ import org.dromara.common.oss.exception.OssException; import org.dromara.common.oss.properties.OssProperties; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.async.AsyncResponseTransformer; -import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; +import software.amazon.awssdk.core.async.*; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; @@ -29,9 +28,12 @@ import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener; import java.io.*; import java.net.URI; import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.Optional; import java.util.function.Consumer; /** @@ -40,6 +42,7 @@ import java.util.function.Consumer; * * @author AprilWind */ +@Slf4j public class OssClient { /** @@ -177,12 +180,12 @@ public class OssClient { // 创建异步请求体(length如果为空会报错) BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder() .contentLength(length) - .subscribeTimeout(Duration.ofSeconds(30)) + .subscribeTimeout(Duration.ofSeconds(120)) .build(); // 使用 transferManager 进行上传 Upload upload = transferManager.upload( - x -> x.requestBody(body) + x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create()) .putObjectRequest( y -> y.bucket(properties.getBucketName()) .key(key) @@ -237,30 +240,61 @@ public class OssClient { * @param key 文件在 Amazon S3 中的对象键 * @param out 输出流 * @param consumer 自定义处理逻辑 - * @return 输出流中写入的字节数(长度) * @throws OssException 如果下载失败,抛出自定义异常 */ public void download(String key, OutputStream out, Consumer consumer) { + try { + this.download(key, consumer).writeTo(out); + } catch (Exception e) { + throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]"); + } + } + + /** + * 下载文件从 Amazon S3 到 输出流 + * + * @param key 文件在 Amazon S3 中的对象键 + * @param contentLengthConsumer 文件大小消费者函数 + * @return 写出订阅器 + * @throws OssException 如果下载失败,抛出自定义异常 + */ + public WriteOutSubscriber download(String key, Consumer contentLengthConsumer) { try { // 构建下载请求 - DownloadRequest> downloadRequest = DownloadRequest.builder() + DownloadRequest> publisherDownloadRequest = DownloadRequest.builder() // 文件对象 .getObjectRequest(y -> y.bucket(properties.getBucketName()) .key(key) .build()) .addTransferListener(LoggingTransferListener.create()) - // 使用订阅转换器 - .responseTransformer(AsyncResponseTransformer.toBlockingInputStream()) + // 使用发布订阅转换器 + .responseTransformer(AsyncResponseTransformer.toPublisher()) .build(); + // 使用 S3TransferManager 下载文件 - Download> responseFuture = transferManager.download(downloadRequest); - // 输出到流中 - try (ResponseInputStream responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream - if (consumer != null) { - consumer.accept(responseStream.response().contentLength()); + Download> publisherDownload = transferManager.download(publisherDownloadRequest); + // 获取下载发布订阅转换器 + ResponsePublisher publisher = publisherDownload.completionFuture().join().result(); + // 执行文件大小消费者函数 + Optional.ofNullable(contentLengthConsumer) + .ifPresent(lengthConsumer -> lengthConsumer.accept(publisher.response().contentLength())); + + // 构建写出订阅器对象 + return out -> { + // 创建可写入的字节通道 + try(WritableByteChannel channel = Channels.newChannel(out)){ + // 订阅数据 + publisher.subscribe(byteBuffer -> { + while (byteBuffer.hasRemaining()) { + try { + channel.write(byteBuffer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }).join(); } - responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread - } + }; } catch (Exception e) { throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]"); } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/WriteOutSubscriber.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/WriteOutSubscriber.java new file mode 100644 index 000000000..d3a9841a1 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/WriteOutSubscriber.java @@ -0,0 +1,15 @@ +package org.dromara.common.oss.core; + +import java.io.IOException; + +/** + * 写出订阅器 + * + * @author 秋辞未寒 + */ +@FunctionalInterface +public interface WriteOutSubscriber { + + void writeTo(T out) throws IOException; + +} diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java index 355cd2931..c433bffad 100644 --- a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java +++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java @@ -129,9 +129,9 @@ public class RedisUtils { } catch (Exception e) { long timeToLive = bucket.remainTimeToLive(); if (timeToLive == -1) { - setCacheObject(key, value); + bucket.set(value); } else { - setCacheObject(key, value, Duration.ofMillis(timeToLive)); + bucket.set(value, Duration.ofMillis(timeToLive)); } } } else { @@ -147,11 +147,8 @@ public class RedisUtils { * @param duration 时间 */ public static void setCacheObject(final String key, final T value, final Duration duration) { - RBatch batch = CLIENT.createBatch(); - RBucketAsync bucket = batch.getBucket(key); - bucket.setAsync(value); - bucket.expireAsync(duration); - batch.execute(); + RBucket bucket = CLIENT.getBucket(key); + bucket.set(value, duration); } /** diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java index 657dbbc07..a3d0badc0 100644 --- a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java +++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java @@ -1,7 +1,7 @@ package org.dromara.common.redis.utils; +import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DatePattern; -import cn.hutool.core.date.DateUtil; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.dromara.common.core.utils.SpringUtils; @@ -10,6 +10,10 @@ import org.redisson.api.RIdGenerator; import org.redisson.api.RedissonClient; import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; /** * 发号器工具类 @@ -23,12 +27,12 @@ public class SequenceUtils { /** * 默认初始值 */ - public static final Long DEFAULT_INIT_VALUE = 1L; + public static final long DEFAULT_INIT_VALUE = 1L; /** * 默认步长 */ - public static final Long DEFAULT_STEP_VALUE = 1L; + public static final long DEFAULT_STEP_VALUE = 1L; /** * 默认过期时间-天 @@ -40,6 +44,11 @@ public class SequenceUtils { */ public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1); + /** + * 默认最小ID容量位数 - 6位数(即至少可以生成的ID为999999个) + */ + public static final int DEFAULT_MIN_ID_CAPACITY_BITS = 6; + /** * 获取Redisson客户端实例 */ @@ -54,14 +63,11 @@ public class SequenceUtils { * @param stepValue ID步长 * @return ID生成器 */ - private static RIdGenerator getIdGenerator(String key, Duration expireTime, Long initValue, Long stepValue) { - if (initValue == null || initValue <= 0) { - initValue = DEFAULT_INIT_VALUE; - } - if (stepValue == null || stepValue <= 0) { - stepValue = DEFAULT_STEP_VALUE; - } + public static RIdGenerator getIdGenerator(String key, Duration expireTime, long initValue, long stepValue) { RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key); + // 初始值和步长不能小于等于0 + initValue = initValue <= 0 ? DEFAULT_INIT_VALUE : initValue; + stepValue = stepValue <= 0 ? DEFAULT_STEP_VALUE : stepValue; // 设置初始值和步长 idGenerator.tryInit(initValue, stepValue); // 设置过期时间 @@ -69,6 +75,17 @@ public class SequenceUtils { return idGenerator; } + /** + * 获取ID生成器 + * + * @param key 业务key + * @param expireTime 过期时间 + * @return ID生成器 + */ + public static RIdGenerator getIdGenerator(String key, Duration expireTime) { + return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE); + } + /** * 获取指定业务key的唯一id * @@ -78,10 +95,21 @@ public class SequenceUtils { * @param stepValue ID步长 * @return 唯一id */ - public static long nextId(String key, Duration expireTime, Long initValue, Long stepValue) { + public static long getNextId(String key, Duration expireTime, long initValue, long stepValue) { return getIdGenerator(key, expireTime, initValue, stepValue).nextId(); } + /** + * 获取指定业务key的唯一id (ID初始值=1,ID步长=1) + * + * @param key 业务key + * @param expireTime 过期时间 + * @return 唯一id + */ + public static long getNextId(String key, Duration expireTime) { + return getIdGenerator(key, expireTime).nextId(); + } + /** * 获取指定业务key的唯一id字符串 * @@ -91,19 +119,8 @@ public class SequenceUtils { * @param stepValue ID步长 * @return 唯一id */ - public static String nextIdStr(String key, Duration expireTime, Long initValue, Long stepValue) { - return String.valueOf(nextId(key, expireTime, initValue, stepValue)); - } - - /** - * 获取指定业务key的唯一id (ID初始值=1,ID步长=1) - * - * @param key 业务key - * @param expireTime 过期时间 - * @return 唯一id - */ - public static long nextId(String key, Duration expireTime) { - return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId(); + public static String getNextIdString(String key, Duration expireTime, long initValue, long stepValue) { + return Convert.toStr(getNextId(key, expireTime, initValue, stepValue)); } /** @@ -113,8 +130,8 @@ public class SequenceUtils { * @param expireTime 过期时间 * @return 唯一id */ - public static String nextIdStr(String key, Duration expireTime) { - return String.valueOf(nextId(key, expireTime)); + public static String getNextIdString(String key, Duration expireTime) { + return Convert.toStr(getNextId(key, expireTime)); } /** @@ -125,56 +142,210 @@ public class SequenceUtils { * @param width 位数,不足左补0 * @return 补零后的唯一id字符串 */ - public static String nextPaddedIdStr(String key, Duration expireTime, Integer width) { - return StringUtils.leftPad(nextIdStr(key, expireTime), width, '0'); + public static String getPaddedNextIdString(String key, Duration expireTime, Integer width) { + return StringUtils.leftPad(getNextIdString(key, expireTime), width, '0'); } /** - * 获取 yyyyMMdd 开头的唯一id + * 获取 yyyyMMdd 格式的唯一id * * @return 唯一id + * @deprecated 请使用 {@link #getDateId(String)} 或 {@link #getDateId(String, boolean)}、{@link #getDateId(String, boolean, int)},确保不同业务的ID连续性 */ - public static String nextIdDate() { - return nextIdDate(""); + @Deprecated + public static String getDateId() { + return getDateId(""); } /** - * 获取 prefix + yyyyMMdd 开头的唯一id + * 获取 prefix + yyyyMMdd 格式的唯一id * * @param prefix 业务前缀 * @return 唯一id */ - public static String nextIdDate(String prefix) { - // 前缀+日期 构建 prefixKey - String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMATTER)); - // 获取下一个id - long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_DAY, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId(); - // 返回完整id - return StringUtils.format("{}{}", prefixKey, nextId); + public static String getDateId(String prefix) { + return getDateId(prefix, true); } /** - * 获取 yyyyMMddHHmmss 开头的唯一id + * 获取 prefix + yyyyMMdd 格式的唯一id * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 * @return 唯一id */ - public static String nextIdDateTime() { - return nextIdDateTime(""); + public static String getDateId(String prefix, boolean isWithPrefix) { + return getDateId(prefix, isWithPrefix, -1); } /** - * 获取 prefix + yyyyMMddHHmmss 开头的唯一id + * 获取 prefix + yyyyMMdd 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}) + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @return 唯一id + */ + public static String getPaddedDateId(String prefix, boolean isWithPrefix) { + return getDateId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS); + } + + /** + * 获取 prefix + yyyyMMdd 格式的唯一id + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) + * @return 唯一id + */ + public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits) { + return getDateId(prefix, isWithPrefix, minIdCapacityBits, LocalDate.now()); + } + + /** + * 获取 prefix + yyyyMMdd 格式的唯一id + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) + * @param time 时间 + * @return 唯一id + */ + public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time) { + return getDateId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE); + } + + /** + * 获取 prefix + yyyyMMdd 格式的唯一id + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) + * @param time 时间 + * @param initValue ID初始值 + * @param stepValue ID步长 + * @return 唯一id + */ + public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time, long initValue, long stepValue) { + return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATE_FORMATTER, DEFAULT_EXPIRE_TIME_DAY, initValue, stepValue); + } + + /** + * 获取 yyyyMMddHHmmss 格式的唯一id + * + * @return 唯一id + * @deprecated 请使用 {@link #getDateTimeId(String)} 或 {@link #getDateTimeId(String, boolean)}、{@link #getDateTimeId(String, boolean, int)},确保不同业务的ID连续性 + */ + @Deprecated + public static String getDateTimeId() { + return getDateTimeId("", false); + } + + /** + * 获取 prefix + yyyyMMddHHmmss 格式的唯一id * * @param prefix 业务前缀 * @return 唯一id */ - public static String nextIdDateTime(String prefix) { - // 前缀+日期时间 构建 prefixKey - String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMATTER)); - // 获取下一个id - long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_MINUTE, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId(); - // 返回完整id - return StringUtils.format("{}{}", prefixKey, nextId); + public static String getDateTimeId(String prefix) { + return getDateTimeId(prefix, true); } + /** + * 获取 prefix + yyyyMMddHHmmss 格式的唯一id + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @return 唯一id + */ + public static String getDateTimeId(String prefix, boolean isWithPrefix) { + return getDateTimeId(prefix, isWithPrefix, -1); + } + + /** + * 获取 prefix + yyyyMMddHHmmss 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}) + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @return 唯一id + */ + public static String getPaddedDateTimeId(String prefix, boolean isWithPrefix) { + return getDateTimeId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS); + } + + /** + * 获取 prefix + yyyyMMddHHmmss 格式的唯一id + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) + * @return 唯一id + */ + public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits) { + return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, LocalDateTime.now()); + } + + /** + * 获取 prefix + yyyyMMddHHmmss 格式的唯一id + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) + * @param time 时间 + * @return 唯一id + */ + public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time) { + return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE); + } + + /** + * 获取 prefix + yyyyMMddHHmmss 格式的唯一id + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) + * @param initValue ID初始值 + * @param stepValue ID步长 + * @return 唯一id + */ + public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time, long initValue, long stepValue) { + return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATETIME_FORMATTER, DEFAULT_EXPIRE_TIME_MINUTE, initValue, stepValue); + } + + /** + * 获取指定业务key的指定时间格式的ID + * + * @param prefix 业务前缀 + * @param isWithPrefix id是否携带业务前缀 + * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位) + * @param temporalAccessor 时间访问器 + * @param timeFormatter 时间格式 + * @param expireTime 过期时间 + * @param initValue ID初始值 + * @param stepValue ID步长 + * @return 唯一id + */ + private static String getDatePatternId(String prefix, boolean isWithPrefix, int minIdCapacityBits, TemporalAccessor temporalAccessor, DateTimeFormatter timeFormatter, Duration expireTime, long initValue, long stepValue) { + // 时间前缀 + String timePrefix = timeFormatter.format(temporalAccessor); + // 业务前缀 + 时间前缀 构建 prefixKey + String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), timePrefix); + + // 获取id,例 -> 1 + String nextId = getNextIdString(prefixKey, expireTime, initValue, stepValue); + + // minIdCapacityBits 大于0,且 nextId 的长度小于 minIdCapacityBits,则左补0 + if (minIdCapacityBits > 0 && nextId.length() < minIdCapacityBits) { + nextId = StringUtils.leftPad(nextId, minIdCapacityBits, '0'); + } + + // 是否携带业务前缀 + if (isWithPrefix) { + // 例 -> P202507031 + // 其中 P 为业务前缀,202507031 为 yyyyMMdd 格式时间, 1 为nextId + return StringUtils.format("{}{}", prefixKey, nextId); + } + // 例 -> 202507031 + // 其中 202507031 为 yyyyMMdd 格式时间, 1 为nextId + return StringUtils.format("{}{}", timePrefix, nextId); + } } diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java index 46c61c4c8..14abf896a 100644 --- a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java +++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java @@ -53,11 +53,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { if (timeout == NEVER_EXPIRE) { RedisUtils.setCacheObject(key, value); } else { - if (RedisUtils.hasKey(key)) { - RedisUtils.setCacheObject(key, value, true); - } else { - RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout)); - } + RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout)); } CAFFEINE.invalidate(key); } @@ -78,7 +74,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { */ @Override public void delete(String key) { - RedisUtils.deleteObject(key); + if (RedisUtils.deleteObject(key)) { + CAFFEINE.invalidate(key); + } } /** @@ -134,11 +132,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { if (timeout == NEVER_EXPIRE) { RedisUtils.setCacheObject(key, object); } else { - if (RedisUtils.hasKey(key)) { - RedisUtils.setCacheObject(key, object, true); - } else { - RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout)); - } + RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout)); } CAFFEINE.invalidate(key); } @@ -159,7 +153,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { */ @Override public void deleteObject(String key) { - RedisUtils.deleteObject(key); + if (RedisUtils.deleteObject(key)) { + CAFFEINE.invalidate(key); + } } /** diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java index e57d1fbde..c9f8c8c2c 100644 --- a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java +++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java @@ -1,6 +1,7 @@ package org.dromara.common.satoken.core.service; import cn.dev33.satoken.stp.StpInterface; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.enums.UserType; @@ -39,8 +40,12 @@ public class SaPermissionImpl implements StpInterface { if (userType == UserType.APP_USER) { // 其他端 自行根据业务编写 } - // SYS_USER 默认返回权限 - return new ArrayList<>(loginUser.getMenuPermission()); + if (CollUtil.isNotEmpty(loginUser.getMenuPermission())) { + // SYS_USER 默认返回权限 + return new ArrayList<>(loginUser.getMenuPermission()); + } else { + return new ArrayList<>(); + } } /** @@ -62,8 +67,12 @@ public class SaPermissionImpl implements StpInterface { if (userType == UserType.APP_USER) { // 其他端 自行根据业务编写 } - // SYS_USER 默认返回权限 - return new ArrayList<>(loginUser.getRolePermission()); + if (CollUtil.isNotEmpty(loginUser.getRolePermission())) { + // SYS_USER 默认返回权限 + return new ArrayList<>(loginUser.getRolePermission()); + } else { + return new ArrayList<>(); + } } private PermissionService getPermissionService() { diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java index 7a2b9fcf6..730bae29d 100644 --- a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java +++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java @@ -207,7 +207,8 @@ public class LoginHelper { */ public static boolean isLogin() { try { - return getLoginUser() != null; + StpUtil.checkLogin(); + return true; } catch (Exception e) { return false; } diff --git a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java index 7af5cee9e..087132ea3 100644 --- a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java +++ b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java @@ -1,5 +1,6 @@ package org.dromara.common.sensitive.core; +import cn.hutool.core.convert.Convert; import cn.hutool.core.util.DesensitizedUtil; import lombok.AllArgsConstructor; @@ -52,7 +53,7 @@ public enum SensitiveStrategy { /** * 用户ID */ - USER_ID(s -> String.valueOf(DesensitizedUtil.userId())), + USER_ID(s -> Convert.toStr(DesensitizedUtil.userId())), /** * 密码 diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java index 3f7924d9b..151d84ce2 100644 --- a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java +++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java @@ -14,6 +14,9 @@ import org.dromara.common.social.gitea.AuthGiteaRequest; import org.dromara.common.social.maxkey.AuthMaxKeyRequest; import org.dromara.common.social.topiam.AuthTopIamRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + /** * 认证授权工具类 * @@ -40,7 +43,7 @@ public class SocialUtils { AuthConfig.AuthConfigBuilder builder = AuthConfig.builder() .clientId(obj.getClientId()) .clientSecret(obj.getClientSecret()) - .redirectUri(obj.getRedirectUri()) + .redirectUri(URLEncoder.encode(obj.getRedirectUri(), StandardCharsets.UTF_8)) .scopes(obj.getScopes()); return switch (source.toLowerCase()) { case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE); diff --git a/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java b/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java index 412834cfb..f77b5b585 100644 --- a/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java +++ b/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.sse.core.SseEmitterManager; -import org.dromara.common.sse.dto.SseMessageDto; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.MediaType; @@ -14,8 +13,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.List; - /** * SSE 控制器 * @@ -33,7 +30,9 @@ public class SseController implements DisposableBean { */ @GetMapping(value = "${sse.path}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter connect() { - StpUtil.checkLogin(); + if (!StpUtil.isLogin()) { + return null; + } String tokenValue = StpUtil.getTokenValue(); Long userId = LoginHelper.getUserId(); return sseEmitterManager.connect(userId, tokenValue); @@ -51,31 +50,32 @@ public class SseController implements DisposableBean { return R.ok(); } - /** - * 向特定用户发送消息 - * - * @param userId 目标用户的 ID - * @param msg 要发送的消息内容 - */ - @GetMapping(value = "${sse.path}/send") - public R send(Long userId, String msg) { - SseMessageDto dto = new SseMessageDto(); - dto.setUserIds(List.of(userId)); - dto.setMessage(msg); - sseEmitterManager.publishMessage(dto); - return R.ok(); - } - - /** - * 向所有用户发送消息 - * - * @param msg 要发送的消息内容 - */ - @GetMapping(value = "${sse.path}/sendAll") - public R send(String msg) { - sseEmitterManager.publishAll(msg); - return R.ok(); - } + // 以下为demo仅供参考 禁止使用 请在业务逻辑中使用工具发送而不是用接口发送 +// /** +// * 向特定用户发送消息 +// * +// * @param userId 目标用户的 ID +// * @param msg 要发送的消息内容 +// */ +// @GetMapping(value = "${sse.path}/send") +// public R send(Long userId, String msg) { +// SseMessageDto dto = new SseMessageDto(); +// dto.setUserIds(List.of(userId)); +// dto.setMessage(msg); +// sseEmitterManager.publishMessage(dto); +// return R.ok(); +// } +// +// /** +// * 向所有用户发送消息 +// * +// * @param msg 要发送的消息内容 +// */ +// @GetMapping(value = "${sse.path}/sendAll") +// public R send(String msg) { +// sseEmitterManager.publishAll(msg); +// return R.ok(); +// } /** * 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误 diff --git a/ruoyi-common/ruoyi-common-web/pom.xml b/ruoyi-common/ruoyi-common-web/pom.xml index b250fa9d0..c687ad589 100644 --- a/ruoyi-common/ruoyi-common-web/pom.xml +++ b/ruoyi-common/ruoyi-common-web/pom.xml @@ -21,11 +21,6 @@ ruoyi-common-json - - org.dromara - ruoyi-common-redis - - org.springframework.boot diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java index 32d61df1c..155a8d071 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java @@ -7,6 +7,7 @@ import org.dromara.common.web.filter.XssFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistration; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -21,24 +22,20 @@ public class FilterConfig { @Bean @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") - public FilterRegistrationBean xssFilterRegistration() { - FilterRegistrationBean registration = new FilterRegistrationBean<>(); - registration.setDispatcherTypes(DispatcherType.REQUEST); - registration.setFilter(new XssFilter()); - registration.addUrlPatterns("/*"); - registration.setName("xssFilter"); - registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1); - return registration; + @FilterRegistration( + name = "xssFilter", + urlPatterns = "/*", + order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1, + dispatcherTypes = DispatcherType.REQUEST + ) + public XssFilter xssFilter() { + return new XssFilter(); } @Bean - public FilterRegistrationBean someFilterRegistration() { - FilterRegistrationBean registration = new FilterRegistrationBean<>(); - registration.setFilter(new RepeatableFilter()); - registration.addUrlPatterns("/*"); - registration.setName("repeatableFilter"); - registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); - return registration; + @FilterRegistration(name = "repeatableFilter", urlPatterns = "/*") + public RepeatableFilter repeatableFilter() { + return new RepeatableFilter(); } } diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java index eda81a7d4..81ec9050a 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java @@ -1,7 +1,8 @@ package org.dromara.common.web.config; +import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; -import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.core.utils.ObjectUtils; import org.dromara.common.web.handler.GlobalExceptionHandler; import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -34,10 +35,11 @@ public class ResourcesConfig implements WebMvcConfigurer { public void addFormatters(FormatterRegistry registry) { // 全局日期格式转换配置 registry.addConverter(String.class, Date.class, source -> { - if (StringUtils.isBlank(source)) { + DateTime parse = DateUtil.parse(source); + if (ObjectUtils.isNull(parse)) { return null; } - return DateUtil.parse(source); + return parse.toJdkDate(); }); } diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java index d0b73345a..d0b6ca3eb 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java @@ -1,8 +1,8 @@ package org.dromara.common.web.enums; import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; import cn.hutool.captcha.generator.RandomGenerator; -import org.dromara.common.web.utils.UnsignedMathGenerator; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,7 +18,7 @@ public enum CaptchaType { /** * 数字 */ - MATH(UnsignedMathGenerator.class), + MATH(MathGenerator.class), /** * 字符 diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/utils/UnsignedMathGenerator.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/utils/UnsignedMathGenerator.java deleted file mode 100644 index a400cff9a..000000000 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/utils/UnsignedMathGenerator.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.dromara.common.web.utils; - -import cn.hutool.captcha.generator.CodeGenerator; -import cn.hutool.core.math.Calculator; -import cn.hutool.core.util.CharUtil; -import cn.hutool.core.util.RandomUtil; -import org.dromara.common.core.utils.StringUtils; - -import java.io.Serial; - -/** - * 无符号计算生成器 - * - * @author Lion Li - */ -public class UnsignedMathGenerator implements CodeGenerator { - - @Serial - private static final long serialVersionUID = -5514819971774091076L; - - private static final String OPERATORS = "+-*"; - - /** - * 参与计算数字最大长度 - */ - private final int numberLength; - - /** - * 构造 - */ - public UnsignedMathGenerator() { - this(2); - } - - /** - * 构造 - * - * @param numberLength 参与计算最大数字位数 - */ - public UnsignedMathGenerator(int numberLength) { - this.numberLength = numberLength; - } - - @Override - public String generate() { - final int limit = getLimit(); - int a = RandomUtil.randomInt(limit); - int b = RandomUtil.randomInt(limit); - String max = Integer.toString(Math.max(a,b)); - String min = Integer.toString(Math.min(a,b)); - max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE); - min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE); - - return max + RandomUtil.randomChar(OPERATORS) + min + '='; - } - - @Override - public boolean verify(String code, String userInputCode) { - int result; - try { - result = Integer.parseInt(userInputCode); - } catch (NumberFormatException e) { - // 用户输入非数字 - return false; - } - - final int calculateResult = (int) Calculator.conversion(code); - return result == calculateResult; - } - - /** - * 获取验证码长度 - * - * @return 验证码长度 - */ - public int getLength() { - return this.numberLength * 2 + 2; - } - - /** - * 根据长度获取参与计算数字最大值 - * - * @return 最大值 - */ - private int getLimit() { - return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength)); - } -} diff --git a/ruoyi-extend/ruoyi-monitor-admin/Dockerfile b/ruoyi-extend/ruoyi-monitor-admin/Dockerfile index f97cb9e86..f39ef9070 100644 --- a/ruoyi-extend/ruoyi-monitor-admin/Dockerfile +++ b/ruoyi-extend/ruoyi-monitor-admin/Dockerfile @@ -1,6 +1,6 @@ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ -FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds -#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds +FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds +#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds #FROM findepi/graalvm:java17-native LABEL maintainer="Lion Li" diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/MonitorAdminApplication.java b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/MonitorAdminApplication.java index 0339ebb06..a7335616c 100644 --- a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/MonitorAdminApplication.java +++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/MonitorAdminApplication.java @@ -1,5 +1,6 @@ package org.dromara.monitor.admin; +import de.codecentric.boot.admin.server.config.EnableAdminServer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -8,6 +9,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; * * @author Lion Li */ +@EnableAdminServer @SpringBootApplication public class MonitorAdminApplication { diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/AdminServerConfig.java b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/AdminServerConfig.java deleted file mode 100644 index 53d248e39..000000000 --- a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/AdminServerConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.dromara.monitor.admin.config; - -import de.codecentric.boot.admin.server.config.EnableAdminServer; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; -import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import java.util.concurrent.Executor; - -/** - * springboot-admin server配置类 - * - * @author Lion Li - */ -@Configuration -@EnableAdminServer -public class AdminServerConfig { - - @Lazy - @Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) - @ConditionalOnMissingBean(Executor.class) - public ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder builder) { - return builder.build(); - } - - -} diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/SecurityConfig.java b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/SecurityConfig.java index 3458cc965..41372f258 100644 --- a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/SecurityConfig.java +++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/SecurityConfig.java @@ -10,7 +10,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; /** * admin 监控 安全配置 @@ -32,14 +32,14 @@ public class SecurityConfig { SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); successHandler.setTargetUrlParameter("redirectTo"); successHandler.setDefaultTargetUrl(adminContextPath + "/"); - + PathPatternRequestMatcher.Builder mvc = PathPatternRequestMatcher.withDefaults(); return httpSecurity .headers((header) -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .authorizeHttpRequests((authorize) -> authorize.requestMatchers( - new AntPathRequestMatcher(adminContextPath + "/assets/**"), - new AntPathRequestMatcher(adminContextPath + "/login") + mvc.matcher(adminContextPath + "/assets/**"), + mvc.matcher(adminContextPath + "/login") ).permitAll() .anyRequest().authenticated()) .formLogin((formLogin) -> diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml b/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml index 622c93d3c..1dd5a8308 100644 --- a/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml +++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml @@ -20,6 +20,9 @@ spring: ui: title: RuoYi-Vue-Plus服务监控中心 context-path: /admin + # 忽略无用警告 + thymeleaf: + check-template-location: false --- # Actuator 监控端点的配置项 management: diff --git a/ruoyi-extend/ruoyi-snailjob-server/Dockerfile b/ruoyi-extend/ruoyi-snailjob-server/Dockerfile index ef85eb2c9..e10ca1834 100644 --- a/ruoyi-extend/ruoyi-snailjob-server/Dockerfile +++ b/ruoyi-extend/ruoyi-snailjob-server/Dockerfile @@ -1,6 +1,6 @@ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ -FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds -#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds +FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds +#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds #FROM findepi/graalvm:java17-native LABEL maintainer="Lion Li" diff --git a/ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/common/register/ServerRegister.java b/ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/common/register/ServerRegister.java new file mode 100644 index 000000000..2a8a47aa4 --- /dev/null +++ b/ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/common/register/ServerRegister.java @@ -0,0 +1,146 @@ +package com.aizuda.snailjob.server.common.register; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.aizuda.snailjob.common.core.enums.NodeTypeEnum; +import com.aizuda.snailjob.common.core.util.JsonUtil; +import com.aizuda.snailjob.common.core.util.NetUtil; +import com.aizuda.snailjob.common.core.util.SnailJobVersion; +import com.aizuda.snailjob.common.core.util.StreamUtils; +import com.aizuda.snailjob.common.log.SnailJobLog; +import com.aizuda.snailjob.server.common.cache.CacheConsumerGroup; +import com.aizuda.snailjob.server.common.config.SystemProperties; +import com.aizuda.snailjob.server.common.convert.RegisterNodeInfoConverter; +import com.aizuda.snailjob.server.common.dto.ServerNodeExtAttrs; +import com.aizuda.snailjob.server.common.handler.InstanceManager; +import com.aizuda.snailjob.template.datasource.persistence.po.ServerNode; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.google.common.collect.Lists; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 服务端注册 + * + * @author opensnail + * @date 2023-06-07 + * @since 1.6.0 + */ +@Component(ServerRegister.BEAN_NAME) +@RequiredArgsConstructor +public class ServerRegister extends AbstractRegister { + public static final String BEAN_NAME = "serverRegister"; + private final ScheduledExecutorService serverRegisterNode = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "server-register-node")); + public static final int DELAY_TIME = 30; + public static final String CURRENT_CID; + public static final String GROUP_NAME = "DEFAULT_SERVER"; + public static final String NAMESPACE_ID = "DEFAULT_SERVER_NAMESPACE_ID"; + private final InstanceManager instanceManager; + private final SystemProperties systemProperties; + private final ServerProperties serverProperties; + + static { + CURRENT_CID = IdUtil.getSnowflakeNextIdStr(); + } + + @Override + public boolean supports(int type) { + return getNodeType().equals(type); + } + + @Override + protected void beforeProcessor(RegisterContext context) { + // 新增扩展参数 + ServerNodeExtAttrs serverNodeExtAttrs = new ServerNodeExtAttrs(); + serverNodeExtAttrs.setWebPort(serverProperties.getPort()); + serverNodeExtAttrs.setSystemVersion(SnailJobVersion.getVersion()); + + context.setGroupName(GROUP_NAME); + context.setHostId(CURRENT_CID); + String serverHost = systemProperties.getServerHost(); + if (StrUtil.isEmptyIfStr(serverHost)) { + serverHost = NetUtil.getLocalIpStr(); + } + context.setHostIp(serverHost); + context.setHostPort(systemProperties.getServerPort()); + context.setContextPath(Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse(StrUtil.EMPTY)); + context.setNamespaceId(NAMESPACE_ID); + context.setExtAttrs(JsonUtil.toJsonString(serverNodeExtAttrs)); + } + + @Override + protected LocalDateTime getExpireAt() { + return LocalDateTime.now().plusSeconds(DELAY_TIME); + } + + @Override + protected boolean doRegister(RegisterContext context, ServerNode serverNode) { + refreshExpireAt(Lists.newArrayList(serverNode)); + return Boolean.TRUE; + } + + + @Override + protected void afterProcessor(final ServerNode serverNode) { + try { + // 同步当前POD消费的组的节点信息 + // netty的client只会注册到一个服务端,若组分配的和client连接的不是一个POD则会导致当前POD没有其他客户端的注册信息 + ConcurrentMap/*namespaceId*/> allConsumerGroupName = CacheConsumerGroup.getAllConsumerGroupName(); + if (CollUtil.isNotEmpty(allConsumerGroupName)) { + Set namespaceIdSets = StreamUtils.toSetByFlatMap(allConsumerGroupName.values(), Set::stream); + if (CollUtil.isEmpty(namespaceIdSets)) { + return; + } + + List serverNodes = serverNodeMapper.selectList( + new LambdaQueryWrapper() + .eq(ServerNode::getNodeType, NodeTypeEnum.CLIENT.getType()) + .in(ServerNode::getNamespaceId, namespaceIdSets) + .in(ServerNode::getGroupName, allConsumerGroupName.keySet())); + for (final ServerNode node : serverNodes) { + // 刷新全量本地缓存 + instanceManager.registerOrUpdate(RegisterNodeInfoConverter.INSTANCE.toRegisterNodeInfo(node)); + // 刷新过期时间 + CacheConsumerGroup.addOrUpdate(node.getGroupName(), node.getNamespaceId()); + } + } + } catch (Exception e) { + SnailJobLog.LOCAL.error("Client refresh failed", e); + } + } + + @Override + protected Integer getNodeType() { + return NodeTypeEnum.SERVER.getType(); + } + + @Override + public void start() { + SnailJobLog.LOCAL.info("ServerRegister start"); + + serverRegisterNode.scheduleAtFixedRate(() -> { + try { + this.register(new RegisterContext()); + } catch (Exception e) { + SnailJobLog.LOCAL.error("Server-side registration failed", e); + } + }, 0, DELAY_TIME * 2 / 3, TimeUnit.SECONDS); + + } + + @Override + public void close() { + SnailJobLog.LOCAL.info("ServerRegister close"); + } +} diff --git a/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml b/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml index aaf474895..18a851c78 100644 --- a/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml +++ b/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml @@ -16,15 +16,26 @@ spring: --- # snail-job 服务端配置 snail-job: - # 拉取重试数据的每批次的大小 - retry-pull-page-size: 1000 - # 拉取重试数据的每批次的大小 - job-pull-page-size: 1000 - # 服务器端口 + # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`) + server-host: + # 服务端端口号 server-port: 17888 - # 日志保存时间(单位: day) + # 合并日志默认保存天数 + merge-Log-days: 1 + # 合并日志默认的条数 + merge-Log-num: 500 + # 配置每批次拉取重试数据的大小 + retry-pull-page-size: 100 + # 配置日志保存时间(单位:天) log-storage: 7 - rpc-type: grpc + # bucket的总数量 + bucket-total: 128 + # Dashboard 任务容错天数 + summary-day: 7 + # 配置负载均衡周期时间 + load-balance-cycle-time: 10 + # 重试任务拉取的并行度 + retry-max-pull-parallel: 2 --- # 监控中心配置 spring.boot.admin.client: diff --git a/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml b/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml index aaf474895..18a851c78 100644 --- a/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml +++ b/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml @@ -16,15 +16,26 @@ spring: --- # snail-job 服务端配置 snail-job: - # 拉取重试数据的每批次的大小 - retry-pull-page-size: 1000 - # 拉取重试数据的每批次的大小 - job-pull-page-size: 1000 - # 服务器端口 + # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`) + server-host: + # 服务端端口号 server-port: 17888 - # 日志保存时间(单位: day) + # 合并日志默认保存天数 + merge-Log-days: 1 + # 合并日志默认的条数 + merge-Log-num: 500 + # 配置每批次拉取重试数据的大小 + retry-pull-page-size: 100 + # 配置日志保存时间(单位:天) log-storage: 7 - rpc-type: grpc + # bucket的总数量 + bucket-total: 128 + # Dashboard 任务容错天数 + summary-day: 7 + # 配置负载均衡周期时间 + load-balance-cycle-time: 10 + # 重试任务拉取的并行度 + retry-max-pull-parallel: 2 --- # 监控中心配置 spring.boot.admin.client: diff --git a/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/logback-plus.xml b/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/logback-plus.xml index be588110c..ed733cb16 100644 --- a/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/logback-plus.xml +++ b/ruoyi-extend/ruoyi-snailjob-server/src/main/resources/logback-plus.xml @@ -85,6 +85,7 @@ + diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailSendController.java similarity index 98% rename from ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailController.java rename to ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailSendController.java index 6fc5a17ba..48365a350 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailController.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailSendController.java @@ -21,7 +21,7 @@ import java.util.Arrays; @RequiredArgsConstructor @RestController @RequestMapping("/demo/mail") -public class MailController { +public class MailSendController { /** * 发送邮件 diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java index 3fd124c78..64243b77c 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java @@ -1,5 +1,6 @@ package org.dromara.demo.controller; +import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.core.collection.CollUtil; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; @@ -14,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -94,6 +96,16 @@ public class TestExcelController { exportExcelService.exportWithOptions(response); } + /** + * 自定义导出 + * + * @param response / + */ + @GetMapping("/customExport") + public void customExport(HttpServletResponse response) throws IOException { + exportExcelService.customExport(response); + } + /** * 多个sheet导出 */ diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java index 4dfa5effa..ad2392b97 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java @@ -2,6 +2,8 @@ package org.dromara.demo.service; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + /** * 导出下拉框Excel示例 * @@ -15,4 +17,11 @@ public interface IExportExcelService { * @param response / */ void exportWithOptions(HttpServletResponse response); + + /** + * 自定义导出 + * + * @param response / + */ + void customExport(HttpServletResponse response) throws IOException; } diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java index 69cf0a8af..2813cc287 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java @@ -2,17 +2,21 @@ package org.dromara.demo.service.impl; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; +import cn.idev.excel.write.metadata.WriteSheet; import jakarta.servlet.http.HttpServletResponse; import lombok.Data; import lombok.RequiredArgsConstructor; import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.utils.StreamUtils; +import org.dromara.common.core.utils.file.FileUtils; import org.dromara.common.excel.core.DropDownOptions; import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.excel.utils.ExcelWriterWrapper; import org.dromara.demo.domain.vo.ExportDemoVo; import org.dromara.demo.service.IExportExcelService; import org.springframework.stereotype.Service; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -233,4 +237,61 @@ public class ExportExcelServiceImpl implements IExportExcelService { this.name = name; } } + + + @Override + public void customExport(HttpServletResponse response) throws IOException { + String filename = ExcelUtil.encodingFilename("自定义导出"); + FileUtils.setAttachmentResponseHeader(response, filename); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"); + + ExcelUtil.exportExcel(ExportDemoVo.class, response.getOutputStream(), wrapper -> { + // 创建表格数据,业务中一般通过数据库查询 + List excelDataList = new ArrayList<>(); + for (int i = 0; i < 30; i++) { + // 模拟数据库中的一条数据 + ExportDemoVo everyRowData = new ExportDemoVo(); + everyRowData.setNickName("用户-" + i); + everyRowData.setUserStatus(SystemConstants.NORMAL); + everyRowData.setGender("1"); + everyRowData.setPhoneNumber(String.format("175%08d", i)); + everyRowData.setEmail(String.format("175%08d", i) + "@163.com"); + everyRowData.setProvinceId(i); + everyRowData.setCityId(i); + everyRowData.setAreaId(i); + excelDataList.add(everyRowData); + } + + // 创建表格 + WriteSheet sheet = ExcelWriterWrapper.sheetBuilder("自定义导出demo") + // 合并单元格 + // .registerWriteHandler(new CellMergeStrategy(excelDataList, true)) + .build(); + + + wrapper.write(excelDataList, sheet); + + List excelDataList2 = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + int index = 1000 + i; + // 模拟数据库中的一条数据 + ExportDemoVo everyRowData = new ExportDemoVo(); + everyRowData.setNickName("用户-" + index); + everyRowData.setUserStatus(SystemConstants.NORMAL); + everyRowData.setGender("1"); + everyRowData.setPhoneNumber(String.format("175%08d", index)); + everyRowData.setEmail(String.format("175%08d", index) + "@163.com"); + everyRowData.setProvinceId(index); + everyRowData.setCityId(index); + everyRowData.setAreaId(index); + excelDataList2.add(everyRowData); + } + + wrapper.write(excelDataList2, sheet); + + // 或者在同一个excel中创建多个表格 + // WriteSheet sheet2 = ExcelWriterWrapper.sheetBuilder("自定义导出demo2").build(); + // wrapper.write(excelDataList2, sheet2); + }); + } } diff --git a/ruoyi-modules/ruoyi-generator/pom.xml b/ruoyi-modules/ruoyi-generator/pom.xml index 49060292f..b93b8371e 100644 --- a/ruoyi-modules/ruoyi-generator/pom.xml +++ b/ruoyi-modules/ruoyi-generator/pom.xml @@ -42,6 +42,11 @@ ruoyi-common-log + + org.dromara + ruoyi-common-idempotent + + org.apache.velocity diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/controller/GenController.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/controller/GenController.java index 64ea78bac..99626a208 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/controller/GenController.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/controller/GenController.java @@ -3,9 +3,11 @@ package org.dromara.generator.controller; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.hutool.core.convert.Convert; import cn.hutool.core.io.IoUtil; +import com.baomidou.lock.annotation.Lock4j; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; +import org.dromara.common.idempotent.annotation.RepeatSubmit; import org.dromara.common.log.annotation.Log; import org.dromara.common.log.enums.BusinessType; import org.dromara.common.mybatis.core.page.PageQuery; @@ -50,6 +52,7 @@ public class GenController extends BaseController { * * @param tableId 表ID */ + @RepeatSubmit() @SaCheckPermission("tool:gen:query") @GetMapping(value = "/{tableId}") public R> getInfo(@PathVariable Long tableId) { @@ -91,6 +94,7 @@ public class GenController extends BaseController { */ @SaCheckPermission("tool:gen:import") @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @RepeatSubmit() @PostMapping("/importTable") public R importTableSave(String tables, String dataName) { String[] tableNames = Convert.toStrArray(tables); @@ -105,6 +109,7 @@ public class GenController extends BaseController { */ @SaCheckPermission("tool:gen:edit") @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @RepeatSubmit() @PutMapping public R editSave(@Validated @RequestBody GenTable genTable) { genTableService.validateEdit(genTable); @@ -170,6 +175,7 @@ public class GenController extends BaseController { */ @SaCheckPermission("tool:gen:edit") @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @Lock4j @GetMapping("/synchDb/{tableId}") public R synchDb(@PathVariable("tableId") Long tableId) { genTableService.synchDb(tableId); diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java index 3572e6f64..c4c79b0ba 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java @@ -302,7 +302,7 @@ public class GenTableServiceImpl implements IGenTableService { tableColumn.setColumnComment(column.getComment()); tableColumn.setColumnType(column.getOriginType().toLowerCase()); tableColumn.setSort(column.getPosition()); - tableColumn.setIsRequired(column.isNullable() ? "1" : "0"); + tableColumn.setIsRequired(column.isNullable() ? "0" : "1"); tableColumn.setIsIncrement(column.isAutoIncrement() ? "1" : "0"); tableColumns.add(tableColumn); }); diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java index 6e111e3a8..44c5979ee 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java @@ -3,6 +3,7 @@ package org.dromara.generator.util; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Dict; +import org.dromara.common.mybatis.enums.DataBaseType; import org.dromara.generator.constant.GenConstants; import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.StringUtils; @@ -118,11 +119,12 @@ public class VelocityUtils { templates.add("vm/java/serviceImpl.java.vm"); templates.add("vm/java/controller.java.vm"); templates.add("vm/xml/mapper.xml.vm"); - if (DataBaseHelper.isOracle()) { + DataBaseType dataBaseType = DataBaseHelper.getDataBaseType(); + if (dataBaseType.isOracle()) { templates.add("vm/sql/oracle/sql.vm"); - } else if (DataBaseHelper.isPostgerSql()) { + } else if (dataBaseType.isPostgreSql()) { templates.add("vm/sql/postgres/sql.vm"); - } else if (DataBaseHelper.isSqlServer()) { + } else if (dataBaseType.isSqlServer()) { templates.add("vm/sql/sqlserver/sql.vm"); } else { templates.add("vm/sql/sql.vm"); diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm index 35a468e80..eeef5f556 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm @@ -54,11 +54,8 @@ export interface ${BusinessName}Query #if(!${treeCode})extends PageQuery #end{ #end #end #end - /** - * 日期范围参数 - */ - params?: any; + /** + * 日期范围参数 + */ + params?: any; } - - - diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm index 4570d46db..fa4e05f63 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -123,7 +123,7 @@ #end #end #end - +