diff --git a/.run/ruoyi-monitor-admin.run.xml b/.run/ruoyi-monitor-admin.run.xml index 751f46f7f..e7d616db7 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 2adc33a19..2d2c4471f 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 442c2748b..5fe048db6 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 457f0a0b5..acd14004c 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,9 @@ [![GitHub](https://img.shields.io/github/stars/dromara/RuoYi-Vue-Plus.svg?style=social&label=Stars)](https://github.com/dromara/RuoYi-Vue-Plus) [![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus) [![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.5.2-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) -[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]() +[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.3-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5-blue.svg)]() [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]() [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]() @@ -33,9 +32,8 @@ MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey
CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow
-数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/
+数舵科技 软件定制开发APP小程序等 - https://www.shuduokeji.com/
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc
-**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/**
Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11
aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong
Ruoyi-Plus-Uniapp - https://ruoyi.plus
diff --git a/pom.xml b/pom.xml index 0c175fda9..5db1e6540 100644 --- a/pom.xml +++ b/pom.xml @@ -13,32 +13,32 @@ Dromara RuoYi-Vue-Plus多租户管理系统 - 5.5.2 - 3.5.9 + 5.5.3 + 3.5.10 UTF-8 UTF-8 17 - 3.5.16 - 2.8.14 + 3.5.19 + 2.8.15 0.15.0 1.3.0 2.3 1.44.0 - 3.5.14 + 3.5.16 3.9.1 - 5.8.40 - 3.5.5 + 5.8.43 + 3.5.6 3.52.0 2.2.7 4.3.1 1.9.0 1.5.0 0.2.0 - 1.18.40 + 1.18.42 1.80 1.16.7 - 3.3.1 + 3.3.2 2.28.22 @@ -226,13 +226,13 @@ s3 ${aws.sdk.version} - + software.amazon.awssdk s3-transfer-manager ${aws.sdk.version} - + software.amazon.awssdk netty-nio-client 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 dcd04c5e1..2586addab 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 @@ -1,8 +1,9 @@ package org.dromara.web.controller; import cn.dev33.satoken.annotation.SaIgnore; -import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.RandomUtil; import jakarta.validation.constraints.NotBlank; @@ -14,14 +15,13 @@ import org.dromara.common.core.domain.R; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StringUtils; -import org.dromara.common.core.utils.reflect.ReflectUtils; import org.dromara.common.mail.config.properties.MailProperties; import org.dromara.common.mail.utils.MailUtils; import org.dromara.common.ratelimiter.annotation.RateLimiter; import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.redis.utils.RedisUtils; +import org.dromara.common.web.core.WaveAndCircleCaptcha; import org.dromara.common.web.config.properties.CaptchaProperties; -import org.dromara.common.web.enums.CaptchaType; import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.core.factory.SmsFactory; @@ -33,6 +33,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import java.awt.*; import java.time.Duration; import java.util.LinkedHashMap; @@ -130,19 +131,21 @@ public class CaptchaController { String uuid = IdUtil.simpleUUID(); String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; // 生成验证码 - CaptchaType captchaType = captchaProperties.getType(); + String captchaType = captchaProperties.getType(); CodeGenerator codeGenerator; - if (CaptchaType.MATH == captchaType) { - codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false); + if ("math".equals(captchaType)) { + codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false); } else { - codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength()); + codeGenerator = new RandomGenerator(captchaProperties.getCharLength()); } - AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); + WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60); + // captcha.setBackground(Color.WHITE); // 不设置就是透明底 + captcha.setFont(new Font("Arial", Font.BOLD, 45)); captcha.setGenerator(codeGenerator); captcha.createCode(); // 如果是数学验证码,使用SpEL表达式处理验证码结果 String code = captcha.getCode(); - if (CaptchaType.MATH == captchaType) { + if ("math".equals(captchaType)) { ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(StringUtils.remove(code, "=")); code = exp.getValue(String.class); diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 866b8f1a2..d11d9f0ea 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -24,9 +24,7 @@ captcha: # 是否启用验证码校验 enable: true # 验证码类型 math 数组计算 char 字符验证 - type: MATH - # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 - category: CIRCLE + type: math # 数字验证码位数 numberLength: 1 # 字符验证码长度 diff --git a/ruoyi-admin/src/main/resources/i18n/messages.properties b/ruoyi-admin/src/main/resources/i18n/messages.properties index f2777f77b..e1e5263b8 100644 --- a/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效 user.not.exists=对不起, 您的账号:{0} 不存在. user.password.not.match=用户不存在/密码错误 user.password.retry.limit.count=密码输入错误{0}次 -user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.retry.limit.exceed=密码输入错误{0}次,账户锁定{1}分钟 user.password.delete=对不起,您的账号:{0} 已被删除 user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员 role.blocked=角色已封禁,请联系管理员 @@ -47,10 +47,10 @@ repeat.submit.message=不允许重复提交,请稍候再试 rate.limiter.message=访问过于频繁,请稍候再试 sms.code.not.blank=短信验证码不能为空 sms.code.retry.limit.count=短信验证码输入错误{0}次 -sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 +sms.code.retry.limit.exceed=短信验证码输入错误{0}次,账户锁定{1}分钟 email.code.not.blank=邮箱验证码不能为空 email.code.retry.limit.count=邮箱验证码输入错误{0}次 -email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟 +email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,账户锁定{1}分钟 xcx.code.not.blank=小程序[code]不能为空 social.source.not.blank=第三方登录平台[source]不能为空 social.code.not.blank=第三方登录平台[code]不能为空 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 f2777f77b..e1e5263b8 100644 --- a/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties +++ b/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties @@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效 user.not.exists=对不起, 您的账号:{0} 不存在. user.password.not.match=用户不存在/密码错误 user.password.retry.limit.count=密码输入错误{0}次 -user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.retry.limit.exceed=密码输入错误{0}次,账户锁定{1}分钟 user.password.delete=对不起,您的账号:{0} 已被删除 user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员 role.blocked=角色已封禁,请联系管理员 @@ -47,10 +47,10 @@ repeat.submit.message=不允许重复提交,请稍候再试 rate.limiter.message=访问过于频繁,请稍候再试 sms.code.not.blank=短信验证码不能为空 sms.code.retry.limit.count=短信验证码输入错误{0}次 -sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 +sms.code.retry.limit.exceed=短信验证码输入错误{0}次,账户锁定{1}分钟 email.code.not.blank=邮箱验证码不能为空 email.code.retry.limit.count=邮箱验证码输入错误{0}次 -email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟 +email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,账户锁定{1}分钟 xcx.code.not.blank=小程序[code]不能为空 social.source.not.blank=第三方登录平台[source]不能为空 social.code.not.blank=第三方登录平台[code]不能为空 diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index 3324cbe5f..dd043681c 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -14,7 +14,7 @@ - 5.5.2 + 5.5.3 diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java index cb5def9a9..393a0f0de 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java @@ -61,7 +61,7 @@ public class UserDTO implements Serializable { private String sex; /** - * 帐号状态(0正常 1停用) + * 账号状态(0正常 1停用) */ private String status; diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java index 191b1e599..5c74a8351 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java @@ -1,6 +1,5 @@ package org.dromara.common.core.utils.ip; -import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.resource.ResourceUtil; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.exception.ServiceException; @@ -9,7 +8,6 @@ import org.lionsoul.ip2region.service.Config; import org.lionsoul.ip2region.service.Ip2Region; import org.lionsoul.ip2region.xdb.Util; -import java.io.File; import java.io.InputStream; import java.time.Duration; @@ -31,6 +29,11 @@ public class RegionUtils { // 下载地址:https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v6.xdb public static final String DEFAULT_IPV6_XDB_PATH = "ip2region_v6.xdb"; + // 默认缓存切片大小为15MB(仅针对BufferCache全量读取有效,如果你的xdb数据库很大,合理设置该值可以有效提升BufferCache模式下的查询效率,具体可以查看Ip2Region的README) + // 注意:设置过大的值可能会申请内存时,因内存不足而导致OOM,请合理设置该值。 + // README:https://gitee.com/lionsoul/ip2region/tree/master/binding/java + public static final int DEFAULT_CACHE_SLICE_BYTES = 1024 * 1024 * 15; + // 未知地址 public static final String UNKNOWN_ADDRESS = "未知"; @@ -43,20 +46,18 @@ public class RegionUtils { // 注意:Ip2Region 的xdb文件加载策略 CachePolicy 有三种,分别是:BufferCache(全量读取xdb到内存中)、VIndexCache(默认策略,按需读取并缓存)、NoCache(实时读取) // 本项目工具使用的 CachePolicy 为 BufferCache,BufferCache会加载整个xdb文件到内存中,setXdbInputStream 仅支持 BufferCache 策略。 // 因为加载整个xdb文件会耗费非常大的内存,如果你不希望加载整个xdb到内存中,更推荐使用 VIndexCache 或 NoCache(即实时读取文件)策略和 setXdbPath/setXdbFile 加载方法(需要注意的一点,setXdbPath 和 setXdbFile 不支持读取ClassPath(即源码和resource目录)中的文件)。 - // 一般而言,更建议把xdb数据库放到一个指定的文件目录中(即不打包进jar包中),然后使用 NoCache + 配合SearcherPool的并发池读取数据,更方便随时更新xdb数据库 + // 一般而言,更建议把xdb数据库放到一个指定的文件目录中(即不打包进jar包中),然后使用 VIndexCache + 配合SearcherPool的并发池读取数据,更方便随时更新xdb数据库 - // TODO 2025年12月23日 Ip2Region封装的 InputStream 读取函数 Searcher.loadContentFromInputStream 在Linux环境下会申请过大的byte[]空间而导致OOM,这里先用临时文件的方案解决,等后续 Ip2Region 更新解决方案 - // 创建临时文件 - File v4TempXdb = FileUtil.writeFromStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH), FileUtil.createTempFile()); + InputStream v4InputStream = ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH); // IPv4配置 Config v4Config = Config.custom() .setCachePolicy(Config.BufferCache) - .setXdbFile(v4TempXdb) -// .setXdbInputStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH)) + //.setXdbFile(v4TempXdb) + .setXdbInputStream(v4InputStream) + // + .setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES) .asV4(); - // 删除临时文件 - v4TempXdb.delete(); // IPv6配置 Config v6Config = null; @@ -64,17 +65,12 @@ public class RegionUtils { if (v6XdbInputStream == null) { log.warn("未加载 IPv6 地址库:未在类路径下找到文件 {}。当前仅启用 IPv4 查询。如需启用 IPv6,请将 ip2region_v6.xdb 放置到 resources 目录", DEFAULT_IPV6_XDB_PATH); } else { - // 创建临时文件 - File v6TempXdb = FileUtil.writeFromStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH), FileUtil.createTempFile()); - v6Config = Config.custom() .setCachePolicy(Config.BufferCache) - .setXdbFile(v6TempXdb) -// .setXdbInputStream(v6XdbInputStream) + //.setXdbFile(v6TempXdb) + .setXdbInputStream(v6XdbInputStream) + .setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES) .asV6(); - - // 删除临时文件 - v6TempXdb.delete(); } // 初始化Ip2Region实例 diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java index a28701fbc..793c0241c 100644 --- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java +++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java @@ -192,7 +192,7 @@ public class MailUtils { /** * 发送邮件给多人 * - * @param mailAccount 邮件帐户信息 + * @param mailAccount 邮件账户信息 * @param tos 收件人列表 * @param subject 标题 * @param content 正文 @@ -207,7 +207,7 @@ public class MailUtils { /** * 发送邮件给多人 * - * @param mailAccount 邮件帐户信息 + * @param mailAccount 邮件账户信息 * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 * @param bccs 密送人列表,可以为null或空 @@ -343,7 +343,7 @@ public class MailUtils { /** * 发送邮件给多人 * - * @param mailAccount 邮件帐户信息 + * @param mailAccount 邮件账户信息 * @param tos 收件人列表 * @param subject 标题 * @param content 正文 @@ -360,7 +360,7 @@ public class MailUtils { /** * 发送邮件给多人 * - * @param mailAccount 邮件帐户信息 + * @param mailAccount 邮件账户信息 * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 * @param bccs 密送人列表,可以为null或空 @@ -400,7 +400,7 @@ public class MailUtils { /** * 发送邮件给多人 * - * @param mailAccount 邮件帐户信息 + * @param mailAccount 邮件账户信息 * @param useGlobalSession 是否全局共享Session * @param tos 收件人列表 * @param ccs 抄送人列表,可以为null或空 diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml index 190dc5d46..bd19cc8d2 100644 --- a/ruoyi-common/ruoyi-common-oss/pom.xml +++ b/ruoyi-common/ruoyi-common-oss/pom.xml @@ -31,7 +31,7 @@ software.amazon.awssdk s3 - + software.amazon.awssdk aws-crt-client @@ -49,13 +49,13 @@ - + software.amazon.awssdk netty-nio-client - + software.amazon.awssdk s3-transfer-manager 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 089d1926b..14fc4dcfe 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 @@ -141,7 +141,8 @@ public class OssClient { try { // 构建上传请求对象 FileUpload fileUpload = transferManager.uploadFile( - x -> x.putObjectRequest( + x -> { + x.source(filePath).putObjectRequest( y -> y.bucket(properties.getBucketName()) .key(key) .contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null) @@ -149,10 +150,13 @@ public class OssClient { // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) //.acl(getAccessPolicy().getObjectCannedACL()) - .build()) - .addTransferListener(LoggingTransferListener.create()) - .source(filePath).build()); - + .build() + ); + if (log.isDebugEnabled()) { + x.addTransferListener(LoggingTransferListener.create()); + } + } + ); // 等待上传完成并获取上传结果 CompletedFileUpload uploadResult = fileUpload.completionFuture().join(); String eTag = uploadResult.response().eTag(); @@ -192,16 +196,21 @@ public class OssClient { // 使用 transferManager 进行上传 Upload upload = transferManager.upload( - x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create()) - .putObjectRequest( + x -> { + x.requestBody(body).putObjectRequest( y -> y.bucket(properties.getBucketName()) .key(key) .contentType(contentType) // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) //.acl(getAccessPolicy().getObjectCannedACL()) - .build()) - .build()); + .build() + ); + if (log.isDebugEnabled()) { + x.addTransferListener(LoggingTransferListener.create()); + } + } + ); // 将输入流写入请求体 body.writeInputStream(inputStream); @@ -229,13 +238,17 @@ public class OssClient { Path tempFilePath = FileUtils.createTempFile().toPath(); // 使用 S3TransferManager 下载文件 FileDownload downloadFile = transferManager.downloadFile( - x -> x.getObjectRequest( + x -> { + x.destination(tempFilePath).getObjectRequest( y -> y.bucket(properties.getBucketName()) .key(removeBaseUrl(path)) - .build()) - .addTransferListener(LoggingTransferListener.create()) - .destination(tempFilePath) - .build()); + .build() + ); + if (log.isDebugEnabled()) { + x.addTransferListener(LoggingTransferListener.create()); + } + } + ); // 等待文件下载操作完成 downloadFile.completionFuture().join(); return tempFilePath; @@ -244,8 +257,8 @@ public class OssClient { /** * 下载文件从 Amazon S3 到 输出流 * - * @param key 文件在 Amazon S3 中的对象键 - * @param out 输出流 + * @param key 文件在 Amazon S3 中的对象键 + * @param out 输出流 * @param consumer 自定义处理逻辑 * @throws OssException 如果下载失败,抛出自定义异常 */ @@ -260,26 +273,24 @@ public class OssClient { /** * 下载文件从 Amazon S3 到 输出流 * - * @param key 文件在 Amazon S3 中的对象键 + * @param key 文件在 Amazon S3 中的对象键 * @param contentLengthConsumer 文件大小消费者函数 * @return 写出订阅器 * @throws OssException 如果下载失败,抛出自定义异常 */ public WriteOutSubscriber download(String key, Consumer contentLengthConsumer) { try { - // 构建下载请求 - DownloadRequest> publisherDownloadRequest = DownloadRequest.builder() - // 文件对象 - .getObjectRequest(y -> y.bucket(properties.getBucketName()) - .key(key) - .build()) - .addTransferListener(LoggingTransferListener.create()) + DownloadRequest.TypedBuilder> typedBuilder = DownloadRequest.builder() // 使用发布订阅转换器 .responseTransformer(AsyncResponseTransformer.toPublisher()) - .build(); + // 文件对象 + .getObjectRequest(y -> y.bucket(properties.getBucketName()).key(key).build()); + if (log.isDebugEnabled()) { + typedBuilder.addTransferListener(LoggingTransferListener.create()); + } // 使用 S3TransferManager 下载文件 - Download> publisherDownload = transferManager.download(publisherDownloadRequest); + Download> publisherDownload = transferManager.download(typedBuilder.build()); // 获取下载发布订阅转换器 ResponsePublisher publisher = publisherDownload.completionFuture().join().result(); // 执行文件大小消费者函数 @@ -289,7 +300,7 @@ public class OssClient { // 构建写出订阅器对象 return out -> { // 创建可写入的字节通道 - try(WritableByteChannel channel = Channels.newChannel(out)){ + try (WritableByteChannel channel = Channels.newChannel(out)) { // 订阅数据 publisher.subscribe(byteBuffer -> { while (byteBuffer.hasRemaining()) { @@ -347,7 +358,7 @@ public class OssClient { * * @param objectKey 对象KEY * @param expiredTime 链接授权到期时间 - * @param metadata 元数据 + * @param metadata 元数据 */ public String createPresignedPutUrl(String objectKey, Duration expiredTime, Map metadata) { // 使用 AWS S3 预签名 URL 的生成器 获取上传文件对象的预签名 URL diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java index 650517ed1..7140db947 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java @@ -1,16 +1,8 @@ package org.dromara.common.web.config; -import cn.hutool.captcha.CaptchaUtil; -import cn.hutool.captcha.CircleCaptcha; -import cn.hutool.captcha.LineCaptcha; -import cn.hutool.captcha.ShearCaptcha; import org.dromara.common.web.config.properties.CaptchaProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; - -import java.awt.*; /** * 验证码配置 @@ -21,45 +13,4 @@ import java.awt.*; @EnableConfigurationProperties(CaptchaProperties.class) public class CaptchaConfig { - private static final int WIDTH = 160; - private static final int HEIGHT = 60; - private static final Color BACKGROUND = Color.LIGHT_GRAY; - private static final Font FONT = new Font("Arial", Font.BOLD, 48); - - /** - * 圆圈干扰验证码 - */ - @Lazy - @Bean - public CircleCaptcha circleCaptcha() { - CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT); - captcha.setBackground(BACKGROUND); - captcha.setFont(FONT); - return captcha; - } - - /** - * 线段干扰的验证码 - */ - @Lazy - @Bean - public LineCaptcha lineCaptcha() { - LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT); - captcha.setBackground(BACKGROUND); - captcha.setFont(FONT); - return captcha; - } - - /** - * 扭曲干扰验证码 - */ - @Lazy - @Bean - public ShearCaptcha shearCaptcha() { - ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT); - captcha.setBackground(BACKGROUND); - captcha.setFont(FONT); - return captcha; - } - } diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java index bfc52f450..6dcfe64bb 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java @@ -1,7 +1,5 @@ package org.dromara.common.web.config.properties; -import org.dromara.common.web.enums.CaptchaCategory; -import org.dromara.common.web.enums.CaptchaType; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -19,12 +17,7 @@ public class CaptchaProperties { /** * 验证码类型 */ - private CaptchaType type; - - /** - * 验证码类别 - */ - private CaptchaCategory category; + private String type; /** * 数字验证码位数 diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/WaveAndCircleCaptcha.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/WaveAndCircleCaptcha.java new file mode 100644 index 000000000..8b37be471 --- /dev/null +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/WaveAndCircleCaptcha.java @@ -0,0 +1,197 @@ +package org.dromara.common.web.core; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.img.GraphicsUtil; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.Serial; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 带干扰线、波浪、圆的验证码 + * + * @author Lion Li + */ +public class WaveAndCircleCaptcha extends AbstractCaptcha { + + @Serial + private static final long serialVersionUID = 1L; + + // 构造方法(略,与之前一致) + public WaveAndCircleCaptcha(int width, int height) { + this(width, height, 4); + } + + public WaveAndCircleCaptcha(int width, int height, int codeCount) { + this(width, height, codeCount, 6); + } + + public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount) { + this(width, height, new RandomGenerator(codeCount), interfereCount); + } + + public WaveAndCircleCaptcha(int width, int height, CodeGenerator generator, int interfereCount) { + super(width, height, generator, interfereCount); + } + + public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount, float size) { + super(width, height, new RandomGenerator(codeCount), interfereCount, size); + } + + @Override + public Image createImage(String code) { + final BufferedImage image = new BufferedImage( + width, + height, + (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB + ); + final Graphics2D g = ImgUtil.createGraphics(image, this.background); + + try { + drawString(g, code); + // 扭曲 + shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + drawInterfere(g); + } finally { + g.dispose(); + } + + return image; + } + + private void drawString(Graphics2D g, String code) { + // 设置抗锯齿(让字体渲染更清晰) + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + if (this.textAlpha != null) { + g.setComposite(this.textAlpha); + } + + GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height); + } + + protected void drawInterfere(Graphics2D g) { + ThreadLocalRandom random = RandomUtil.getRandom(); + int circleCount = Math.max(0, this.interfereCount - 1); + + // 圈圈 + for (int i = 0; i < circleCount; i++) { + g.setColor(ImgUtil.randomColor(random)); + int x = random.nextInt(width); + int y = random.nextInt(height); + int w = random.nextInt(height >> 1); + int h = random.nextInt(height >> 1); + g.drawOval(x, y, w, h); + } + + // 仅 1 条平滑波浪线 + if (this.interfereCount >= 1) { + g.setColor(getRandomColor(120, 230, random)); + drawSmoothWave(g, random); + } + } + + private void drawSmoothWave(Graphics2D g, ThreadLocalRandom random) { + int amplitude = random.nextInt(8) + 5; // 波动幅度 + int wavelength = random.nextInt(40) + 30; // 波长 + double phase = random.nextDouble() * Math.PI * 2; + + // ✅ 关键:限制 baseY 在中间区域 + int centerY = height / 2; + int verticalJitter = Math.max(5, height / 6); // 至少偏移5像素 + int baseY = centerY - verticalJitter + random.nextInt(verticalJitter * 2); + + g.setStroke(new BasicStroke(2.5f)); // 线宽 + + int[] xPoints = new int[width]; + int[] yPoints = new int[width]; + for (int x = 0; x < width; x++) { + int y = baseY + (int) (amplitude * Math.sin((double) x / wavelength * 2 * Math.PI + phase)); + // 限制 y 不要超出图像边界(可选) + y = Math.max(amplitude, Math.min(y, height - amplitude)); + xPoints[x] = x; + yPoints[x] = y; + } + g.drawPolyline(xPoints, yPoints, width); + } + + private Color getRandomColor(int min, int max, ThreadLocalRandom random) { + int range = max - min; + return new Color( + min + random.nextInt(range), + min + random.nextInt(range), + min + random.nextInt(range) + ); + } + + /** + * 扭曲 + * + * @param g {@link Graphics} + * @param w1 w1 + * @param h1 h1 + * @param color 颜色 + */ + private void shear(Graphics g, int w1, int h1, Color color) { + shearX(g, w1, h1, color); + shearY(g, w1, h1, color); + } + + /** + * X坐标扭曲 + * + * @param g {@link Graphics} + * @param w1 宽 + * @param h1 高 + * @param color 颜色 + */ + private void shearX(Graphics g, int w1, int h1, Color color) { + + int period = RandomUtil.randomInt(this.width); + + int frames = 1; + int phase = RandomUtil.randomInt(2); + + for (int i = 0; i < h1; i++) { + double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(0, i, w1, 1, (int) d, 0); + g.setColor(color); + g.drawLine((int) d, i, 0, i); + g.drawLine((int) d + w1, i, w1, i); + } + + } + + /** + * Y坐标扭曲 + * + * @param g {@link Graphics} + * @param w1 宽 + * @param h1 高 + * @param color 颜色 + */ + private void shearY(Graphics g, int w1, int h1, Color color) { + + int period = RandomUtil.randomInt(this.height >> 1); + + int frames = 20; + int phase = 7; + for (int i = 0; i < w1; i++) { + double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(i, 0, 1, h1, 0, (int) d); + g.setColor(color); + // 擦除原位置的痕迹 + g.drawLine(i, (int) d, i, 0); + g.drawLine(i, (int) d + h1, i, h1); + } + + } + +} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaCategory.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaCategory.java deleted file mode 100644 index ecf26583c..000000000 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaCategory.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.dromara.common.web.enums; - -import cn.hutool.captcha.AbstractCaptcha; -import cn.hutool.captcha.CircleCaptcha; -import cn.hutool.captcha.LineCaptcha; -import cn.hutool.captcha.ShearCaptcha; -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 验证码类别 - * - * @author Lion Li - */ -@Getter -@AllArgsConstructor -public enum CaptchaCategory { - - /** - * 线段干扰 - */ - LINE(LineCaptcha.class), - - /** - * 圆圈干扰 - */ - CIRCLE(CircleCaptcha.class), - - /** - * 扭曲干扰 - */ - SHEAR(ShearCaptcha.class); - - private final Class clazz; -} 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 deleted file mode 100644 index d0b6ca3eb..000000000 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java +++ /dev/null @@ -1,29 +0,0 @@ -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 lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 验证码类型 - * - * @author Lion Li - */ -@Getter -@AllArgsConstructor -public enum CaptchaType { - - /** - * 数字 - */ - MATH(MathGenerator.class), - - /** - * 字符 - */ - CHAR(RandomGenerator.class); - - private final Class clazz; -} 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 deleted file mode 100644 index 2a8a47aa4..000000000 --- a/ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/common/register/ServerRegister.java +++ /dev/null @@ -1,146 +0,0 @@ -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-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysMenuController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysMenuController.java index d1b5b9bd5..7d0ca0068 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysMenuController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysMenuController.java @@ -137,6 +137,8 @@ public class SysMenuController extends BaseController { return R.fail("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); } else if (SystemConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) { return R.fail("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } else if (!menuService.checkRouteConfigUnique(menu)) { + return R.fail("新增菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在"); } return toAjax(menuService.insertMenu(menu)); } @@ -156,6 +158,8 @@ public class SysMenuController extends BaseController { return R.fail("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); } else if (menu.getMenuId().equals(menu.getParentId())) { return R.fail("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } else if (!menuService.checkRouteConfigUnique(menu)) { + return R.fail("修改菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在"); } return toAjax(menuService.updateMenu(menu)); } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysMenu.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysMenu.java index 2df55962b..5fe0de585 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysMenu.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysMenu.java @@ -130,11 +130,11 @@ public class SysMenu extends BaseEntity { public String getRouterPath() { String routerPath = this.path; // 内链打开外网方式 - if (getParentId() != 0L && isInnerLink()) { + if (!Constants.TOP_PARENT_ID.equals(getParentId()) && isInnerLink()) { routerPath = innerLinkReplaceEach(routerPath); } // 非外链并且是一级目录(类型为目录) - if (0L == getParentId() && SystemConstants.TYPE_DIR.equals(getMenuType()) + if (Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_DIR.equals(getMenuType()) && SystemConstants.NO_FRAME.equals(getIsFrame())) { routerPath = "/" + this.path; } @@ -152,7 +152,7 @@ public class SysMenu extends BaseEntity { String component = SystemConstants.LAYOUT; if (StringUtils.isNotEmpty(this.component) && !isMenuFrame()) { component = this.component; - } else if (StringUtils.isEmpty(this.component) && getParentId() != 0L && isInnerLink()) { + } else if (StringUtils.isEmpty(this.component) && !Constants.TOP_PARENT_ID.equals(getParentId()) && isInnerLink()) { component = SystemConstants.INNER_LINK; } else if (StringUtils.isEmpty(this.component) && isParentView()) { component = SystemConstants.PARENT_VIEW; @@ -164,7 +164,7 @@ public class SysMenu extends BaseEntity { * 是否为菜单内部跳转 */ public boolean isMenuFrame() { - return getParentId() == 0L && SystemConstants.TYPE_MENU.equals(menuType) && isFrame.equals(SystemConstants.NO_FRAME); + return Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_MENU.equals(menuType) && isFrame.equals(SystemConstants.NO_FRAME); } /** @@ -178,7 +178,7 @@ public class SysMenu extends BaseEntity { * 是否为parent_view组件 */ public boolean isParentView() { - return getParentId() != 0L && SystemConstants.TYPE_DIR.equals(menuType); + return !Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_DIR.equals(menuType); } /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java index 3712f805f..a06172f66 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java @@ -78,7 +78,7 @@ public class SysUser extends TenantEntity { private String password; /** - * 帐号状态(0正常 1停用) + * 账号状态(0正常 1停用) */ private String status; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java index 1472d2428..11c01666d 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java @@ -78,7 +78,7 @@ public class SysUserBo extends BaseEntity { private String password; /** - * 帐号状态(0正常 1停用) + * 账号状态(0正常 1停用) */ private String status; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java index 913ab2001..c60087202 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java @@ -63,9 +63,9 @@ public class SysUserExportVo implements Serializable { private String sex; /** - * 帐号状态(0正常 1停用) + * 账号状态(0正常 1停用) */ - @ExcelProperty(value = "帐号状态", converter = ExcelDictConvert.class) + @ExcelProperty(value = "账号状态", converter = ExcelDictConvert.class) @ExcelDictFormat(dictType = "sys_normal_disable") private String status; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java index d4e575c14..4507f631e 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java @@ -67,9 +67,9 @@ public class SysUserImportVo implements Serializable { private String sex; /** - * 帐号状态(0正常 1停用) + * 账号状态(0正常 1停用) */ - @ExcelProperty(value = "帐号状态", converter = ExcelDictConvert.class) + @ExcelProperty(value = "账号状态", converter = ExcelDictConvert.class) @ExcelDictFormat(dictType = "sys_normal_disable") private String status; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java index 86249d20e..755dcf81d 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java @@ -89,7 +89,7 @@ public class SysUserVo implements Serializable { private String password; /** - * 帐号状态(0正常 1停用) + * 账号状态(0正常 1停用) */ private String status; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysMenuService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysMenuService.java index f972691be..8888c3c39 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysMenuService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysMenuService.java @@ -160,4 +160,13 @@ public interface ISysMenuService { * @return 结果 */ boolean checkMenuNameUnique(SysMenuBo menu); + + /** + * 校验路由组合是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + boolean checkRouteConfigUnique(SysMenuBo menu); + } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java index 1fe554547..9e255f992 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java @@ -174,7 +174,7 @@ public interface ISysUserService { * 修改用户状态 * * @param userId 用户ID - * @param status 帐号状态 + * @param status 账号状态 * @return 结果 */ int updateUserStatus(Long userId, String status); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java index bbba6a01e..0c69b1f3f 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java @@ -6,6 +6,7 @@ import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.utils.MapstructUtils; @@ -29,13 +30,17 @@ import org.dromara.system.service.ISysMenuService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; /** * 菜单 业务层处理 * * @author Lion Li */ +@Slf4j @RequiredArgsConstructor @Service public class SysMenuServiceImpl implements ISysMenuService { @@ -353,6 +358,51 @@ public class SysMenuServiceImpl implements ISysMenuService { return !exist; } + /** + * 校验路由名称是否唯一 + * + * @param menuBo 菜单信息 + * @return 结果 + */ + @Override + public boolean checkRouteConfigUnique(SysMenuBo menuBo) { + SysMenu menu = MapstructUtils.convert(menuBo, SysMenu.class); + if (SystemConstants.TYPE_BUTTON.equals(menu.getMenuType())) { + return true; + } + long menuId = ObjectUtil.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + Long parentId = menu.getParentId(); + String path = menu.getPath(); + String routeName = StringUtils.isEmpty(menu.getRouteName()) ? path : menu.getRouteName(); + List sysMenuList = baseMapper.selectList( + new LambdaQueryWrapper() + .in(SysMenu::getMenuType, SystemConstants.TYPE_DIR, SystemConstants.TYPE_MENU) + .and(w -> + w.eq(SysMenu::getPath, path).or().eq(SysMenu::getPath, routeName) + )); + for (SysMenu sysMenu : sysMenuList) { + if (!sysMenu.getMenuId().equals(menuId)) { + Long dbParentId = sysMenu.getParentId(); + String dbPath = sysMenu.getPath(); + String dbRouteName = StringUtils.isEmpty(sysMenu.getRouteName()) ? dbPath : sysMenu.getRouteName(); + if (StringUtils.equalsAnyIgnoreCase(path, dbPath) && parentId.equals(dbParentId)) { + log.warn("[同级路由冲突] 同级下已存在相同路由路径 '{}',冲突菜单:{}", dbPath, sysMenu.getMenuName()); + return false; + } else if (StringUtils.equalsAnyIgnoreCase(path, dbPath) + && Constants.TOP_PARENT_ID.equals(parentId) + && Constants.TOP_PARENT_ID.equals(dbParentId)) { + log.warn("[根目录路由冲突] 根目录下路由 '{}' 必须唯一,已被菜单 '{}' 占用", path, sysMenu.getMenuName()); + return false; + } else if (StringUtils.equalsAnyIgnoreCase(routeName, dbRouteName) + && sysMenu.getMenuType().equals(menu.getMenuType())) { + log.warn("[路由名称冲突] 路由名称 '{}' 需全局唯一,已被菜单 '{}' 使用", routeName, sysMenu.getMenuName()); + return false; + } + } + } + return true; + } + /** * 根据父节点的ID获取所有子节点 * diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java index aafc73e29..39ad4cb91 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java @@ -375,7 +375,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService { * 修改用户状态 * * @param userId 用户ID - * @param status 帐号状态 + * @param status 账号状态 * @return 结果 */ @Override diff --git a/script/docker/docker-compose.yml b/script/docker/docker-compose.yml index 462650bd5..f40a4ff01 100644 --- a/script/docker/docker-compose.yml +++ b/script/docker/docker-compose.yml @@ -99,7 +99,7 @@ services: network_mode: "host" ruoyi-server1: - image: ruoyi/ruoyi-server:5.5.2 + image: ruoyi/ruoyi-server:5.5.3 container_name: ruoyi-server1 environment: # 时区上海 @@ -115,7 +115,7 @@ services: network_mode: "host" ruoyi-server2: - image: ruoyi/ruoyi-server:5.5.2 + image: ruoyi/ruoyi-server:5.5.3 container_name: ruoyi-server2 environment: # 时区上海 @@ -131,7 +131,7 @@ services: network_mode: "host" ruoyi-monitor-admin: - image: ruoyi/ruoyi-monitor-admin:5.5.2 + image: ruoyi/ruoyi-monitor-admin:5.5.3 container_name: ruoyi-monitor-admin environment: # 时区上海 @@ -143,7 +143,7 @@ services: network_mode: "host" ruoyi-snailjob-server: - image: ruoyi/ruoyi-snailjob-server:5.5.2 + image: ruoyi/ruoyi-snailjob-server:5.5.3 container_name: ruoyi-snailjob-server environment: # 时区上海 diff --git a/script/sql/oracle/oracle_ry_vue_5.X.sql b/script/sql/oracle/oracle_ry_vue_5.X.sql index 041e99a57..3a9cef456 100644 --- a/script/sql/oracle/oracle_ry_vue_5.X.sql +++ b/script/sql/oracle/oracle_ry_vue_5.X.sql @@ -257,7 +257,7 @@ comment on column sys_user.phonenumber is '手机号码'; comment on column sys_user.sex is '用户性别(0男 1女 2未知)'; comment on column sys_user.avatar is '头像路径'; comment on column sys_user.password is '密码'; -comment on column sys_user.status is '帐号状态(0正常 1停用)'; +comment on column sys_user.status is '账号状态(0正常 1停用)'; comment on column sys_user.del_flag is '删除标志(0代表存在 1代表删除)'; comment on column sys_user.login_ip is '最后登录IP'; comment on column sys_user.login_date is '最后登录时间'; diff --git a/script/sql/postgres/postgres_ry_vue_5.X.sql b/script/sql/postgres/postgres_ry_vue_5.X.sql index b5da8e096..bb127f9e2 100644 --- a/script/sql/postgres/postgres_ry_vue_5.X.sql +++ b/script/sql/postgres/postgres_ry_vue_5.X.sql @@ -258,7 +258,7 @@ comment on column sys_user.phonenumber is '手机号码'; comment on column sys_user.sex is '用户性别(0男 1女 2未知)'; comment on column sys_user.avatar is '头像地址'; comment on column sys_user.password is '密码'; -comment on column sys_user.status is '帐号状态(0正常 1停用)'; +comment on column sys_user.status is '账号状态(0正常 1停用)'; comment on column sys_user.del_flag is '删除标志(0代表存在 1代表删除)'; comment on column sys_user.login_ip is '最后登陆IP'; comment on column sys_user.login_date is '最后登陆时间'; diff --git a/script/sql/ry_vue_5.X.sql b/script/sql/ry_vue_5.X.sql index dae47920d..3cd642b0b 100644 --- a/script/sql/ry_vue_5.X.sql +++ b/script/sql/ry_vue_5.X.sql @@ -148,7 +148,7 @@ create table sys_user ( sex char(1) default '0' comment '用户性别(0男 1女 2未知)', avatar bigint(20) comment '头像地址', password varchar(100) default '' comment '密码', - status char(1) default '0' comment '帐号状态(0正常 1停用)', + status char(1) default '0' comment '账号状态(0正常 1停用)', del_flag char(1) default '0' comment '删除标志(0代表存在 1代表删除)', login_ip varchar(128) default '' comment '最后登录IP', login_date datetime comment '最后登录时间', diff --git a/script/sql/sqlserver/sqlserver_ry_vue_5.X.sql b/script/sql/sqlserver/sqlserver_ry_vue_5.X.sql index 5db798848..295cc8e36 100644 --- a/script/sql/sqlserver/sqlserver_ry_vue_5.X.sql +++ b/script/sql/sqlserver/sqlserver_ry_vue_5.X.sql @@ -2802,7 +2802,7 @@ EXEC sys.sp_addextendedproperty 'COLUMN', N'password' GO EXEC sys.sp_addextendedproperty - 'MS_Description', N'帐号状态(0正常 1停用)' , + 'MS_Description', N'账号状态(0正常 1停用)' , 'SCHEMA', N'dbo', 'TABLE', N'sys_user', 'COLUMN', N'status'