diff --git a/README.md b/README.md index e617bc81c..47c8c3b6f 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ ## 赞助商 -MaxKey - https://gitee.com/dromara/MaxKey
-CCFlow - https://gitee.com/opencc/RuoYi-JFlow
+MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey
+CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow
+数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/
[如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group) # 本框架与RuoYi的功能差异 diff --git a/pom.xml b/pom.xml index de3d6e94e..ba9ced2d7 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ 5.2.0-SNAPSHOT - 3.2.1 + 3.2.3 UTF-8 UTF-8 17 @@ -27,15 +27,15 @@ 1.37.0 3.5.5 3.9.1 - 5.8.24 + 5.8.26 4.10.0 - 3.2.0 - 3.25.2 - 2.2.5 + 3.2.2 + 3.27.0 + 2.2.7 4.3.0 2.14.4 4.3.6 - 1.3.5 + 1.3.6 0.2.0 1.18.30 1.76 @@ -44,9 +44,10 @@ 2.7.0 - 1.12.600 + 2.23.0 + 0.29.6 - 2.2.0 + 3.1.1 1.2.83 @@ -246,10 +247,23 @@ ${okhttp.version} + - com.amazonaws - aws-java-sdk-s3 - ${aws-java-sdk-s3.version} + software.amazon.awssdk + s3 + ${aws.sdk.version} + + + + software.amazon.awssdk.crt + aws-crt + ${aws.crt.version} + + + + software.amazon.awssdk + s3-transfer-manager + ${aws.sdk.version} 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 a18ab50fe..43e689be3 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 @@ -23,8 +23,8 @@ import org.dromara.common.social.config.properties.SocialLoginConfigProperties; import org.dromara.common.social.config.properties.SocialProperties; import org.dromara.common.social.utils.SocialUtils; import org.dromara.common.tenant.helper.TenantHelper; +import org.dromara.common.websocket.dto.WebSocketMessageDto; import org.dromara.common.websocket.utils.WebSocketUtils; -import org.dromara.system.domain.SysClient; import org.dromara.system.domain.bo.SysTenantBo; import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysTenantVo; @@ -97,7 +97,10 @@ public class AuthController { Long userId = LoginHelper.getUserId(); scheduledExecutorService.schedule(() -> { - WebSocketUtils.sendMessage(userId, "欢迎登录RuoYi-Vue-Plus后台管理系统"); + WebSocketMessageDto dto = new WebSocketMessageDto(); + dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统"); + dto.setSessionKeys(List.of(userId)); + WebSocketUtils.publishMessage(dto); }, 3, TimeUnit.SECONDS); return R.ok(loginVo); } 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 ef33f5b6f..1a476a94a 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 @@ -5,6 +5,9 @@ import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.RandomUtil; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.domain.R; @@ -21,11 +24,7 @@ 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; -import org.dromara.sms4j.provider.enumerate.SupplierType; import org.dromara.web.domain.vo.CaptchaVo; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -66,11 +65,11 @@ public class CaptchaController { String templateId = ""; LinkedHashMap map = new LinkedHashMap<>(1); map.put("code", code); - SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA); + SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map); - if (!"OK".equals(smsResponse.getCode())) { + if (!smsResponse.isSuccess()) { log.error("验证码短信发送异常 => {}", smsResponse); - return R.fail(smsResponse.getMessage()); + return R.fail(smsResponse.getData().toString()); } return R.ok(); } @@ -121,6 +120,7 @@ public class CaptchaController { AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); captcha.setGenerator(codeGenerator); captcha.createCode(); + // 如果是数学验证码,使用SpEL表达式处理验证码结果 String code = captcha.getCode(); if (isMath) { ExpressionParser parser = new SpelExpressionParser(); diff --git a/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java b/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java index 4d4bc89eb..db9c2712c 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java @@ -13,10 +13,19 @@ import lombok.Data; @AutoMapper(target = SysTenantVo.class) public class TenantListVo { + /** + * 租户编号 + */ private String tenantId; + /** + * 企业名称 + */ private String companyName; + /** + * 域名 + */ private String domain; } diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java index 44eaece58..a75b9131e 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java @@ -18,6 +18,11 @@ public interface IAuthStrategy { /** * 登录 + * + * @param body 登录对象 + * @param client 授权管理视图对象 + * @param grantType 授权类型 + * @return 登录验证信息 */ static LoginVo login(String body, SysClientVo client, String grantType) { // 授权类型和客户端id @@ -31,6 +36,10 @@ public interface IAuthStrategy { /** * 登录 + * + * @param body 登录对象 + * @param client 授权管理视图对象 + * @return 登录验证信息 */ LoginVo login(String body, SysClientVo client); diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java b/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java index f9b7f2c69..fa8b1db2c 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java @@ -145,10 +145,12 @@ public class SysLoginService { loginUser.setUserType(user.getUserType()); loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId())); loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId())); - SysDeptVo dept = deptService.selectDeptById(user.getDeptId()); - loginUser.setDeptName(ObjectUtil.isNull(dept) ? "" : dept.getDeptName()); - List roles = roleService.selectRolesByUserId(user.getUserId()); - loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class)); + TenantHelper.dynamic(user.getTenantId(), () -> { + SysDeptVo dept = deptService.selectDeptById(user.getDeptId()); + loginUser.setDeptName(ObjectUtil.isNull(dept) ? "" : dept.getDeptName()); + List roles = roleService.selectRolesByUserId(user.getUserId()); + loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class)); + }); return loginUser; } 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 f38d074e3..14fa0cc27 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 @@ -82,7 +82,7 @@ public class SysRegisterService { * @param uuid 唯一标识 */ public void validateCaptcha(String tenantId, String username, String code, String uuid) { - String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, ""); + String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, ""); String captcha = RedisUtils.getCacheObject(verifyKey); RedisUtils.deleteObject(verifyKey); if (captcha == null) { diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 69ee3995d..5589e5864 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -149,36 +149,40 @@ mail: connectionTimeout: 0 --- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商 -# https://wind.kim/doc/start 文档地址 各个厂商可同时使用 +# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用 sms: - # 阿里云 dysmsapi.aliyuncs.com - alibaba: - #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置 - requestUrl: dysmsapi.aliyuncs.com - #阿里云的accessKey - accessKeyId: xxxxxxx - #阿里云的accessKeySecret - accessKeySecret: xxxxxxx - #短信签名 - signature: 测试 - tencent: - #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置 - requestUrl: sms.tencentcloudapi.com - #腾讯云的accessKey - accessKeyId: xxxxxxx - #腾讯云的accessKeySecret - accessKeySecret: xxxxxxx - #短信签名 - signature: 测试 - #短信sdkAppId - sdkAppId: appid - #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置 - territory: ap-guangzhou + # 配置源类型用于标定配置来源(interface,yaml) + config-type: yaml + # 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制 + restricted: true + # 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效 + minute-max: 1 + # 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效 + account-max: 30 + # 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中 + blends: + # 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可 + # 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户 + config1: + # 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分 + supplier: alibaba + # 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。 + access-key-id: 您的accessKey + # 称为accessSecret有些称之为apiSecret + access-key-secret: 您的accessKeySecret + signature: 您的短信签名 + sdk-app-id: 您的sdkAppId + config2: + # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分 + supplier: tencent + access-key-id: 您的accessKey + access-key-secret: 您的accessKeySecret + signature: 您的短信签名 + sdk-app-id: 您的sdkAppId --- # 三方授权 justauth: - enabled: true # 前端外网访问地址 address: http://localhost:80 type: diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index c2cbd2d5d..b4f4040bd 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -152,35 +152,39 @@ mail: connectionTimeout: 0 --- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商 -# https://wind.kim/doc/start 文档地址 各个厂商可同时使用 +# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用 sms: - # 阿里云 dysmsapi.aliyuncs.com - alibaba: - #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置 - requestUrl: dysmsapi.aliyuncs.com - #阿里云的accessKey - accessKeyId: xxxxxxx - #阿里云的accessKeySecret - accessKeySecret: xxxxxxx - #短信签名 - signature: 测试 - tencent: - #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置 - requestUrl: sms.tencentcloudapi.com - #腾讯云的accessKey - accessKeyId: xxxxxxx - #腾讯云的accessKeySecret - accessKeySecret: xxxxxxx - #短信签名 - signature: 测试 - #短信sdkAppId - sdkAppId: appid - #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置 - territory: ap-guangzhou + # 配置源类型用于标定配置来源(interface,yaml) + config-type: yaml + # 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制 + restricted: true + # 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效 + minute-max: 1 + # 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效 + account-max: 30 + # 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中 + blends: + # 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可 + # 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户 + config1: + # 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分 + supplier: alibaba + # 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。 + access-key-id: 您的accessKey + # 称为accessSecret有些称之为apiSecret + access-key-secret: 您的accessKeySecret + signature: 您的短信签名 + sdk-app-id: 您的sdkAppId + config2: + # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分 + supplier: tencent + access-key-id: 您的accessKey + access-key-secret: 您的accessKeySecret + signature: 您的短信签名 + sdk-app-id: 您的sdkAppId --- # 三方授权 justauth: - enabled: true # 前端外网访问地址 address: http://localhost:80 type: @@ -191,6 +195,13 @@ justauth: client-id: 876892492581044224 client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8 redirect-uri: ${justauth.address}/social-callback?source=maxkey + topiam: + # topiam 服务器地址 + server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol + client-id: 449c4*********937************759 + client-secret: ac7***********1e0************28d + redirect-uri: ${justauth.address}/social-callback?source=topiam + scopes: [ openid, email, phone, profile ] qq: client-id: 10**********6 client-secret: 1f7d08**********5b7**********29e diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 7cc02071c..a3ea740be 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -47,6 +47,7 @@ logging: org.dromara: @logging.level@ org.springframework: warn tech.powerjob.worker.background: warn + org.mybatis.spring.mapper: error config: classpath:logback-plus.xml # 用户配置 @@ -63,7 +64,6 @@ spring: name: ${ruoyi.name} threads: # 开启虚拟线程 仅jdk21可用 - # 开启后还需更改 UndertowConfig 虚拟线程配置 virtual: enabled: false # 资源信息 @@ -143,8 +143,7 @@ tenant: # MyBatisPlus配置 # https://baomidou.com/config/ mybatis-plus: - # 不支持多包, 如有需要可在注解配置 或 提升扫包等级 - # 例如 com.**.**.mapper + # 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper mapperPackage: org.dromara.**.mapper # 对应的 XML 文件位置 mapperLocations: classpath*:mapper/**/*Mapper.xml @@ -231,6 +230,7 @@ xss: urlPatterns: /system/*,/monitor/*,/tool/* # 全局线程池相关配置 +# 如使用JDK21请直接使用虚拟线程 不要开启此配置 thread-pool: # 是否开启线程池 enabled: false diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java index 07500ba1b..d12008706 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java @@ -2,6 +2,7 @@ package org.dromara.common.core.config; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.scheduling.annotation.EnableAsync; /** * 程序注解配置 @@ -11,6 +12,7 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy; @AutoConfiguration // 表示通过aop框架暴露该代理对象,AopContext能够访问 @EnableAspectJAutoProxy(exposeProxy = true) +@EnableAsync(proxyTargetClass = true) public class ApplicationConfig { } 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 index 9a32afe55..cd01e33d5 100644 --- 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 @@ -5,18 +5,19 @@ 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 org.springframework.scheduling.annotation.EnableAsync; import java.util.Arrays; import java.util.concurrent.Executor; /** * 异步配置 + *

+ * 如果未使用虚拟线程则生效 * * @author Lion Li */ -@EnableAsync(proxyTargetClass = true) @AutoConfiguration public class AsyncConfig implements AsyncConfigurer { @@ -25,6 +26,9 @@ public class AsyncConfig implements AsyncConfigurer { */ @Override public Executor getAsyncExecutor() { + if(SpringUtils.isVirtual()) { + return new VirtualThreadTaskExecutor("async-"); + } return SpringUtils.getBean("scheduledExecutorService"); } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java new file mode 100644 index 000000000..b13c0571d --- /dev/null +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java @@ -0,0 +1,49 @@ +package org.dromara.common.core.constant; + +import cn.hutool.core.lang.RegexPool; + +/** + * 常用正则表达式字符串 + *

+ * 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/ + * + * @author Feng + */ +public interface RegexConstants extends RegexPool { + + /** + * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线) + */ + public static final String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$"; + + /** + * 身份证号码(后6位) + */ + public static final String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"; + + /** + * QQ号码 + */ + public static final String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$"; + + /** + * 邮政编码 + */ + public static final String POSTAL_CODE = "^[1-9]\\d{5}$"; + + /** + * 注册账号 + */ + public static final String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$"; + + /** + * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符 + */ + public static final String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"; + + /** + * 通用状态(0表示正常,1表示停用) + */ + public static final String STATUS = "^[01]$"; + +} diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java new file mode 100644 index 000000000..fd907d2c2 --- /dev/null +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java @@ -0,0 +1,52 @@ +package org.dromara.common.core.factory; + +import cn.hutool.core.lang.PatternPool; +import org.dromara.common.core.constant.RegexConstants; + +import java.util.regex.Pattern; + +/** + * 正则表达式模式池工厂 + *

初始化的时候将正则表达式加入缓存池当中

+ *

提高正则表达式的性能,避免重复编译相同的正则表达式

+ * + * @author 21001 + */ +public class RegexPatternPoolFactory extends PatternPool { + + /** + * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线) + */ + public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE); + + /** + * 身份证号码(后6位) + */ + public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6); + + /** + * QQ号码 + */ + public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER); + + /** + * 邮政编码 + */ + public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE); + + /** + * 注册账号 + */ + public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT); + + /** + * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符 + */ + public static final Pattern PASSWORD = get(RegexConstants.PASSWORD); + + /** + * 通用状态(0表示正常,1表示停用) + */ + public static final Pattern STATUS = get(RegexConstants.STATUS); + +} 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 d6b312a64..f8f155a7b 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 @@ -23,4 +23,20 @@ public interface UserService { */ String selectNicknameById(Long userId); + /** + * 通过用户ID查询用户手机号 + * + * @param userId 用户id + * @return 用户手机号 + */ + String selectPhonenumberById(Long userId); + + /** + * 通过用户ID查询用户邮箱 + * + * @param userId 用户id + * @return 用户邮箱 + */ + String selectEmailById(Long userId); + } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java index ab5053931..e58c394ac 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java @@ -3,7 +3,9 @@ package org.dromara.common.core.utils; import cn.hutool.extra.spring.SpringUtil; import org.springframework.aop.framework.AopContext; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; /** @@ -59,4 +61,8 @@ public final class SpringUtils extends SpringUtil { return getApplicationContext(); } + public static boolean isVirtual() { + return Threading.VIRTUAL.isActive(getBean(Environment.class)); + } + } 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 5e4db50e3..dd6ebb119 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 @@ -22,6 +22,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { public static final String SEPARATOR = ","; + public static final String SLASH = "/"; + /** * 获取参数不为空值 * diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java new file mode 100644 index 000000000..b8b12d43c --- /dev/null +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java @@ -0,0 +1,30 @@ +package org.dromara.common.core.utils.regex; + + +import cn.hutool.core.util.ReUtil; +import org.dromara.common.core.constant.RegexConstants; + +/** + * 正则相关工具类 + * + * @author Feng + */ +public final class RegexUtils extends ReUtil { + + /** + * 从输入字符串中提取匹配的部分,如果没有匹配则返回默认值 + * + * @param input 要提取的输入字符串 + * @param regex 用于匹配的正则表达式,可以使用 {@link RegexConstants} 中定义的常量 + * @param defaultInput 如果没有匹配时返回的默认值 + * @return 如果找到匹配的部分,则返回匹配的部分,否则返回默认值 + */ + public static String extractFromString(String input, String regex, String defaultInput) { + try { + return ReUtil.get(regex, input, 1); + } catch (Exception e) { + return defaultInput; + } + } + +} diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java new file mode 100644 index 000000000..c0dda2020 --- /dev/null +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java @@ -0,0 +1,105 @@ +package org.dromara.common.core.utils.regex; + +import cn.hutool.core.exceptions.ValidateException; +import cn.hutool.core.lang.Validator; +import org.dromara.common.core.factory.RegexPatternPoolFactory; + +import java.util.regex.Pattern; + +/** + * 正则字段校验器 + * 主要验证字段非空、是否为满足指定格式等 + * + * @author Feng + */ +public class RegexValidator extends Validator { + + /** + * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线) + */ + public static final Pattern DICTIONARY_TYPE = RegexPatternPoolFactory.DICTIONARY_TYPE; + + /** + * 身份证号码(后6位) + */ + public static final Pattern ID_CARD_LAST_6 = RegexPatternPoolFactory.ID_CARD_LAST_6; + + /** + * QQ号码 + */ + public static final Pattern QQ_NUMBER = RegexPatternPoolFactory.QQ_NUMBER; + + /** + * 邮政编码 + */ + public static final Pattern POSTAL_CODE = RegexPatternPoolFactory.POSTAL_CODE; + + /** + * 注册账号 + */ + public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT; + + /** + * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符 + */ + public static final Pattern PASSWORD = RegexPatternPoolFactory.PASSWORD; + + /** + * 通用状态(0表示正常,1表示停用) + */ + public static final Pattern STATUS = RegexPatternPoolFactory.STATUS; + + + /** + * 检查输入的账号是否匹配预定义的规则 + * + * @param value 要验证的账号 + * @return 如果账号符合规则,返回 true;否则,返回 false。 + */ + public static boolean isAccount(CharSequence value) { + return isMatchRegex(ACCOUNT, value); + } + + /** + * 验证输入的账号是否符合规则,如果不符合,则抛出 ValidateException 异常 + * + * @param value 要验证的账号 + * @param errorMsg 验证失败时抛出的异常消息 + * @param CharSequence 的子类型 + * @return 如果验证通过,返回输入的账号 + * @throws ValidateException 如果验证失败 + */ + public static T validateAccount(T value, String errorMsg) throws ValidateException { + if (!isAccount(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 检查输入的状态是否匹配预定义的规则 + * + * @param value 要验证的状态 + * @return 如果状态符合规则,返回 true;否则,返回 false。 + */ + public static boolean isStatus(CharSequence value) { + return isMatchRegex(STATUS, value); + } + + /** + * 验证输入的状态是否符合规则,如果不符合,则抛出 ValidateException 异常 + * + * @param value 要验证的状态 + * @param errorMsg 验证失败时抛出的异常消息 + * @param CharSequence 的子类型 + * @return 如果验证通过,返回输入的状态 + * @throws ValidateException 如果验证失败 + */ + public static T validateStatus(T value, String errorMsg) throws ValidateException { + if (!isStatus(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + +} diff --git a/ruoyi-common/ruoyi-common-encrypt/pom.xml b/ruoyi-common/ruoyi-common-encrypt/pom.xml index df3222bee..ed4910ef0 100644 --- a/ruoyi-common/ruoyi-common-encrypt/pom.xml +++ b/ruoyi-common/ruoyi-common-encrypt/pom.xml @@ -22,11 +22,6 @@ ruoyi-common-core
- - org.mybatis.spring.boot - mybatis-spring-boot-starter - - org.bouncycastle bcprov-jdk15to18 @@ -42,6 +37,18 @@ spring-webmvc + + com.baomidou + mybatis-plus-spring-boot3-starter + true + + + org.mybatis + mybatis-spring + + + + diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java index e988a3a2c..fbc4e52de 100644 --- a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java +++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java @@ -1,5 +1,8 @@ package org.dromara.common.encrypt.config; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.encrypt.core.EncryptorManager; import org.dromara.common.encrypt.interceptor.MybatisDecryptInterceptor; import org.dromara.common.encrypt.interceptor.MybatisEncryptInterceptor; @@ -16,17 +19,18 @@ import org.springframework.context.annotation.Bean; * @author 老马 * @version 4.6.0 */ -@AutoConfiguration +@AutoConfiguration(after = MybatisPlusAutoConfiguration.class) @EnableConfigurationProperties(EncryptorProperties.class) @ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true") +@Slf4j public class EncryptorAutoConfiguration { @Autowired private EncryptorProperties properties; @Bean - public EncryptorManager encryptorManager() { - return new EncryptorManager(); + public EncryptorManager encryptorManager(MybatisPlusProperties mybatisPlusProperties) { + return new EncryptorManager(mybatisPlusProperties.getTypeAliasesPackage()); } @Bean @@ -38,4 +42,8 @@ public class EncryptorAutoConfiguration { public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorManager encryptorManager) { return new MybatisDecryptInterceptor(encryptorManager, 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 498b4b85b..a6d3cf9ce 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 @@ -1,14 +1,23 @@ package org.dromara.common.encrypt.core; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; 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.utils.StringUtils; import org.dromara.common.encrypt.annotation.EncryptField; +import org.springframework.context.ConfigurableApplicationContext; +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.util.ClassUtils; import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -19,6 +28,7 @@ import java.util.stream.Collectors; * @version 4.6.0 */ @Slf4j +@NoArgsConstructor public class EncryptorManager { /** @@ -31,25 +41,24 @@ public class EncryptorManager { */ Map, Set> fieldCache = new ConcurrentHashMap<>(); + /** + * 构造方法传入类加密字段缓存 + * + * @param typeAliasesPackage 实体类包 + */ + public EncryptorManager(String typeAliasesPackage) { + scanEncryptClasses(typeAliasesPackage); + } + + /** * 获取类加密字段缓存 */ public Set getFieldCache(Class sourceClazz) { - return fieldCache.computeIfAbsent(sourceClazz, clazz -> { - Set fieldSet = new HashSet<>(); - while (clazz != null) { - Field[] fields = clazz.getDeclaredFields(); - fieldSet.addAll(Arrays.asList(fields)); - clazz = clazz.getSuperclass(); - } - fieldSet = fieldSet.stream().filter(field -> - field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class) - .collect(Collectors.toSet()); - for (Field field : fieldSet) { - field.setAccessible(true); - } - return fieldSet; - }); + if (ObjectUtil.isNotNull(fieldCache)) { + return fieldCache.get(sourceClazz); + } + return null; } /** @@ -97,4 +106,53 @@ public class EncryptorManager { return encryptor.decrypt(value); } + /** + * 通过 typeAliasesPackage 设置的扫描包 扫描缓存实体 + */ + private void scanEncryptClasses(String typeAliasesPackage) { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(); + String[] packagePatternArray = StringUtils.splitPreserveAllTokens(typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX; + try { + for (String packagePattern : packagePatternArray) { + String path = ClassUtils.convertClassNameToResourcePath(packagePattern); + Resource[] resources = resolver.getResources(classpath + path + "/*.class"); + for (Resource resource : resources) { + ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata(); + Class clazz = Resources.classForName(classMetadata.getClassName()); + Set encryptFieldSet = getEncryptFieldSetFromClazz(clazz); + if (CollUtil.isNotEmpty(encryptFieldSet)) { + fieldCache.put(clazz, encryptFieldSet); + } + } + } + } catch (Exception e) { + log.error("初始化数据安全缓存时出错:{}", e.getMessage()); + } + } + + /** + * 获得一个类的加密字段集合 + */ + private Set getEncryptFieldSetFromClazz(Class clazz) { + Set fieldSet = new HashSet<>(); + // 判断clazz如果是接口,内部类,匿名类就直接返回 + if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) { + return fieldSet; + } + while (clazz != null) { + Field[] fields = clazz.getDeclaredFields(); + fieldSet.addAll(Arrays.asList(fields)); + clazz = clazz.getSuperclass(); + } + fieldSet = fieldSet.stream().filter(field -> + field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class) + .collect(Collectors.toSet()); + for (Field field : fieldSet) { + field.setAccessible(true); + } + return fieldSet; + } + } diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java index b43881a22..16d24fc21 100644 --- a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java +++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java @@ -18,7 +18,6 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.io.IOException; -import java.io.PrintWriter; /** @@ -37,8 +36,9 @@ public class CryptoFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest servletRequest = (HttpServletRequest) request; HttpServletResponse servletResponse = (HttpServletResponse) response; - - boolean responseFlag = false; + // 获取加密注解 + ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest); + boolean responseFlag = apiEncrypt != null && apiEncrypt.response(); ServletRequest requestWrapper = null; ServletResponse responseWrapper = null; EncryptResponseBodyWrapper responseBodyWrapper = null; @@ -49,9 +49,6 @@ public class CryptoFilter implements Filter { if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) { // 是否存在加密标头 String headerValue = servletRequest.getHeader(properties.getHeaderFlag()); - // 获取加密注解 - ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest); - responseFlag = apiEncrypt != null && apiEncrypt.response(); if (StringUtils.isNotBlank(headerValue)) { // 请求解密 requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag()); @@ -65,13 +62,13 @@ public class CryptoFilter implements Filter { return; } } - // 判断是否响应加密 - if (responseFlag) { - responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse); - responseWrapper = responseBodyWrapper; - } } } + // 判断是否响应加密 + if (responseFlag) { + responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse); + responseWrapper = responseBodyWrapper; + } chain.doFilter( ObjectUtil.defaultIfNull(requestWrapper, request), diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java index 7c2508f86..460aa360e 100644 --- a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java +++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java @@ -73,7 +73,11 @@ public class MybatisDecryptInterceptor implements Interceptor { list.forEach(this::decryptHandler); return; } + // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错) Set fields = encryptorManager.getFieldCache(sourceObject.getClass()); + if(ObjectUtil.isNull(fields)){ + return; + } try { for (Field field : fields) { field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field)); diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java index 152f7db40..bcc2f4c9f 100644 --- a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java +++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java @@ -82,7 +82,11 @@ public class MybatisEncryptInterceptor implements Interceptor { list.forEach(this::encryptHandler); return; } + // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错) Set fields = encryptorManager.getFieldCache(sourceObject.getClass()); + if(ObjectUtil.isNull(fields)){ + return; + } try { for (Field field : fields) { field.set(sourceObject, this.encryptField(Convert.toStr(field.get(sourceObject)), field)); diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java index bbdaaa14d..6b9211b53 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java @@ -21,4 +21,9 @@ public @interface CellMerge { */ int index() default -1; + /** + * 合并需要依赖的其他字段名称 + */ + String[] mergeBy() default {}; + } 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 bcb5be767..7c0a48b9a 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,8 +1,12 @@ 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 com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.metadata.Head; +import com.alibaba.excel.write.handler.WorkbookWriteHandler; +import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext; import com.alibaba.excel.write.merge.AbstractMergeStrategy; import lombok.AllArgsConstructor; import lombok.Data; @@ -15,10 +19,7 @@ import org.dromara.common.core.utils.reflect.ReflectUtils; import org.dromara.common.excel.annotation.CellMerge; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * 列值重复合并策略 @@ -26,7 +27,7 @@ import java.util.Map; * @author Lion Li */ @Slf4j -public class CellMergeStrategy extends AbstractMergeStrategy { +public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler { private final List cellList; private final boolean hasTitle; @@ -41,17 +42,28 @@ public class CellMergeStrategy extends AbstractMergeStrategy { @Override protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { - // judge the list is not null - if (CollUtil.isNotEmpty(cellList)) { - // the judge is necessary - if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) { - for (CellRangeAddress item : cellList) { - sheet.addMergedRegion(item); + //单元格写入了,遍历合并区域,如果该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(); } } } } + @Override + public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) { + //当前表格写完后,统一写入 + if (CollUtil.isNotEmpty(cellList)){ + for (CellRangeAddress item : cellList) { + context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item); + } + } + } + @SneakyThrows private List handle(List list, boolean hasTitle) { List cellList = new ArrayList<>(); @@ -93,35 +105,15 @@ public class CellMergeStrategy extends AbstractMergeStrategy { // 空值跳过不合并 continue; } + if (!cellValue.equals(val)) { - if (i - repeatCell.getCurrent() > 1) { + if ((i - repeatCell.getCurrent() > 1) && isMerge(list, i, field)) { cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); } map.put(field, new RepeatCell(val, i)); - } else if (j == 0) { - if (i == list.size() - 1) { - if (i > repeatCell.getCurrent()) { - cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum)); - } - } - } else { - // 判断前面的是否合并了 - RepeatCell firstCell = map.get(mergeFields.get(0)); - if (repeatCell.getCurrent() != firstCell.getCurrent()) { - if (i == list.size() - 1) { - if (i > repeatCell.getCurrent()) { - cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum)); - } - } else if (repeatCell.getCurrent() < firstCell.getCurrent()) { - 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 (i > repeatCell.getCurrent()) { - cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum)); - } + } else if (i == list.size() - 1) { + if (i > repeatCell.getCurrent() && isMerge(list, i, field)) { + cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum)); } } } @@ -130,6 +122,24 @@ public class CellMergeStrategy extends AbstractMergeStrategy { 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 { 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 3b791ea54..b3f68ed1e 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 @@ -20,6 +20,7 @@ import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.service.DictService; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StreamUtils; +import org.dromara.common.core.utils.StringUtils; import org.dromara.common.excel.annotation.ExcelDictFormat; import org.dromara.common.excel.annotation.ExcelEnumFormat; @@ -99,15 +100,16 @@ public class ExcelDownHandler implements SheetWriteHandler { ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class); String dictType = format.dictType(); String converterExp = format.readConverterExp(); - if (StrUtil.isNotBlank(dictType)) { + if (StringUtils.isNotBlank(dictType)) { // 如果传递了字典名,则依据字典建立下拉 Collection values = Optional.ofNullable(dictService.getAllDictByDictType(dictType)) .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType))) .values(); options = new ArrayList<>(values); - } else if (StrUtil.isNotBlank(converterExp)) { + } else if (StringUtils.isNotBlank(converterExp)) { // 如果指定了确切的值,则直接解析确切的值 - options = StrUtil.split(converterExp, format.separator(), true, true); + List strList = StringUtils.splitList(converterExp, format.separator()); + options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]); } } else if (field.isAnnotationPresent(ExcelEnumFormat.class)) { // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑 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 1791b2b99..c465c2a4b 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 @@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.dromara.common.core.factory.YmlPropertySourceFactory; +import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler; import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor; import org.mybatis.spring.annotation.MapperScan; @@ -41,7 +42,7 @@ public class MybatisPlusConfig { * 数据权限拦截器 */ public PlusDataPermissionInterceptor dataPermissionInterceptor() { - return new PlusDataPermissionInterceptor(); + return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage")); } /** diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java index 9018a791c..2e6208196 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java @@ -35,7 +35,6 @@ public interface BaseMapperPlus extends BaseMapper { Log log = LogFactory.getLog(BaseMapperPlus.class); default Class currentVoClass() { - GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class); return (Class) GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class)[1]; } diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java index 63653de3c..a66908f09 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java @@ -29,16 +29,17 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler { ? baseEntity.getCreateTime() : new Date(); baseEntity.setCreateTime(current); baseEntity.setUpdateTime(current); - LoginUser loginUser = getLoginUser(); - if (ObjectUtil.isNotNull(loginUser)) { - Long userId = ObjectUtil.isNotNull(baseEntity.getCreateBy()) - ? baseEntity.getCreateBy() : loginUser.getUserId(); - // 当前已登录 且 创建人为空 则填充 - baseEntity.setCreateBy(userId); - // 当前已登录 且 更新人为空 则填充 - baseEntity.setUpdateBy(userId); - baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept()) - ? baseEntity.getCreateDept() : loginUser.getDeptId()); + if (ObjectUtil.isNull(baseEntity.getCreateBy())) { + LoginUser loginUser = getLoginUser(); + if (ObjectUtil.isNotNull(loginUser)) { + Long userId = loginUser.getUserId(); + // 当前已登录 且 创建人为空 则填充 + baseEntity.setCreateBy(userId); + // 当前已登录 且 更新人为空 则填充 + baseEntity.setUpdateBy(userId); + baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept()) + ? baseEntity.getCreateDept() : loginUser.getDeptId()); + } } } } catch (Exception e) { @@ -53,11 +54,12 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler { Date current = new Date(); // 更新时间填充(不管为不为空) baseEntity.setUpdateTime(current); - LoginUser loginUser = getLoginUser(); // 当前已登录 更新人填充(不管为不为空) - if (ObjectUtil.isNotNull(loginUser)) { - baseEntity.setUpdateBy(loginUser.getUserId()); + Long userId = LoginHelper.getUserId(); + if (ObjectUtil.isNotNull(userId)) { + baseEntity.setUpdateBy(userId); } + } } catch (Exception e) { throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED); diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java index c517fa728..ec3ee0df7 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java @@ -2,6 +2,7 @@ package org.dromara.common.mybatis.handler; import org.dromara.common.core.domain.R; import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.StringUtils; import org.mybatis.spring.MyBatisSystemException; import org.springframework.dao.DuplicateKeyException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -35,7 +36,7 @@ public class MybatisExceptionHandler { public R handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); String message = e.getMessage(); - if ("CannotFindDataSourceException".contains(message)) { + if (StringUtils.contains("CannotFindDataSourceException", message)) { log.error("请求地址'{}', 未找到数据源", requestURI); return R.fail("未找到数据源,请联系管理员确认"); } 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 6ddaa243c..7d7fd84a9 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 @@ -2,7 +2,6 @@ package org.dromara.common.mybatis.handler; import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ObjectUtil; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.JSQLParserException; @@ -10,6 +9,7 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Parenthesis; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; 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; @@ -21,16 +21,26 @@ 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.BeanResolver; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParserContext; 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.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -58,9 +68,13 @@ public class PlusDataPermissionHandler { */ private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory()); + public PlusDataPermissionHandler(String mapperPackage) { + scanMapperClasses(mapperPackage); + } + public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) { - DataColumn[] dataColumns = findAnnotation(mappedStatementId); + DataPermission dataPermission = getDataPermission(mappedStatementId); LoginUser currentUser = DataPermissionHelper.getVariable("user"); if (ObjectUtil.isNull(currentUser)) { currentUser = LoginHelper.getLoginUser(); @@ -70,7 +84,7 @@ public class PlusDataPermissionHandler { if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) { return where; } - String dataFilterSql = buildDataFilter(dataColumns, isSelect); + String dataFilterSql = buildDataFilter(dataPermission.value(), isSelect); if (StringUtils.isBlank(dataFilterSql)) { return where; } @@ -144,43 +158,64 @@ public class PlusDataPermissionHandler { return ""; } - public DataColumn[] findAnnotation(String mappedStatementId) { - StringBuilder sb = new StringBuilder(mappedStatementId); - int index = sb.lastIndexOf("."); - String clazzName = sb.substring(0, index); - String methodName = sb.substring(index + 1, sb.length()); - Class clazz; + /** + * 通过 mapperPackage 设置的扫描包 扫描缓存有注解的方法与类 + */ + private void scanMapperClasses(String mapperPackage) { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(); + String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX; try { - clazz = ClassUtil.loadClass(clazzName); + for (String packagePattern : packagePatternArray) { + String path = ClassUtils.convertClassNameToResourcePath(packagePattern); + 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) { - return null; + log.error("初始化数据安全缓存时出错:{}", e.getMessage()); } - List methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz)) - .filter(method -> method.getName().equals(methodName)).toList(); + } + + private void findAnnotation(Class clazz) { DataPermission dataPermission; // 获取方法注解 - for (Method method : methods) { - dataPermission = dataPermissionCacheMap.get(mappedStatementId); - if (ObjectUtil.isNotNull(dataPermission)) { - return dataPermission.value(); + 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); - return dataPermission.value(); } } - dataPermission = dataPermissionCacheMap.get(clazz.getName()); - if (ObjectUtil.isNotNull(dataPermission)) { - return dataPermission.value(); - } // 获取类注解 if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) { dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class); dataPermissionCacheMap.put(clazz.getName(), dataPermission); - return dataPermission.value(); + } + } + + public DataPermission getDataPermission(String mapperId) { + if (dataPermissionCacheMap.containsKey(mapperId)) { + return dataPermissionCacheMap.get(mapperId); + } + String clazzName = mapperId.substring(0, mapperId.lastIndexOf(".")); + if (dataPermissionCacheMap.containsKey(clazzName)) { + return dataPermissionCacheMap.get(clazzName); } return null; } + /** + * 是否无效 + */ + public boolean invalid(String mapperId) { + return getDataPermission(mapperId) == null; + } } 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 0ab0c1139..f287846ce 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 @@ -1,13 +1,10 @@ package org.dromara.common.mybatis.interceptor; -import cn.hutool.core.collection.ConcurrentHashSet; -import cn.hutool.core.util.ArrayUtil; import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper; import com.baomidou.mybatisplus.core.toolkit.PluginUtils; import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; -import org.dromara.common.mybatis.annotation.DataColumn; -import org.dromara.common.mybatis.handler.PlusDataPermissionHandler; +import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -22,11 +19,11 @@ import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; +import org.dromara.common.mybatis.handler.PlusDataPermissionHandler; import java.sql.Connection; import java.sql.SQLException; import java.util.List; -import java.util.Set; /** * 数据权限拦截器 @@ -34,13 +31,14 @@ import java.util.Set; * @author Lion Li * @version 3.5.0 */ +@Slf4j public class PlusDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor { - private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler(); - /** - * 无效注解方法缓存用于快速返回 - */ - private final Set invalidCacheSet = new ConcurrentHashSet<>(); + private final PlusDataPermissionHandler dataPermissionHandler; + + public PlusDataPermissionInterceptor(String mapperPackage) { + this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage); + } @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { @@ -49,12 +47,7 @@ public class PlusDataPermissionInterceptor extends JsqlParserSupport implements return; } // 检查是否无效 无数据权限注解 - if (invalidCacheSet.contains(ms.getId())) { - return; - } - DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId()); - if (ArrayUtil.isEmpty(dataColumns)) { - invalidCacheSet.add(ms.getId()); + if (dataPermissionHandler.invalid(ms.getId())) { return; } // 解析 sql 分配对应方法 @@ -72,12 +65,7 @@ public class PlusDataPermissionInterceptor extends JsqlParserSupport implements return; } // 检查是否无效 无数据权限注解 - if (invalidCacheSet.contains(ms.getId())) { - return; - } - DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId()); - if (ArrayUtil.isEmpty(dataColumns)) { - invalidCacheSet.add(ms.getId()); + if (dataPermissionHandler.invalid(ms.getId())) { return; } PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/resources/spy.properties b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/spy.properties index abbd8931d..e9d10f3f1 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/resources/spy.properties +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/resources/spy.properties @@ -6,8 +6,6 @@ logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql #appender=com.p6spy.engine.spy.appender.Slf4JLogger -# 设置 p6spy driver 代理 -#deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. @@ -16,12 +14,6 @@ excludecategories=info,debug,result,commit,resultset dateformat=yyyy-MM-dd HH:mm:ss # SQL语句打印时间格式 databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss -# 实际驱动可多个 -#driverlist=org.h2.Driver -# 是否开启慢SQL记录 -outagedetection=true -# 慢SQL记录标准 2 秒 -outagedetectioninterval=2 # 是否过滤 Log filter=true # 过滤 Log 时所排除的 sql 关键字,以逗号分隔 diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml index 8e7affff4..18d004f57 100644 --- a/ruoyi-common/ruoyi-common-oss/pom.xml +++ b/ruoyi-common/ruoyi-common-oss/pom.xml @@ -26,10 +26,46 @@ ruoyi-common-redis + - com.amazonaws - aws-java-sdk-s3 + software.amazon.awssdk + s3 + + + + software.amazon.awssdk + netty-nio-client + + + + software.amazon.awssdk + aws-crt-client + + + + software.amazon.awssdk + apache-client + + + + software.amazon.awssdk + url-connection-client + + + + + + software.amazon.awssdk.crt + aws-crt + + + + + 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 53e05c927..eb1656c06 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,73 +2,114 @@ package org.dromara.common.oss.core; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.IdUtil; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.HttpMethod; -import com.amazonaws.Protocol; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.*; +import org.dromara.common.core.constant.Constants; import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.core.utils.file.FileUtils; import org.dromara.common.oss.constant.OssConstant; import org.dromara.common.oss.entity.UploadResult; import org.dromara.common.oss.enumd.AccessPolicyType; import org.dromara.common.oss.enumd.PolicyType; 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.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.model.*; +import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener; import java.io.ByteArrayInputStream; import java.io.File; +import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URL; -import java.util.Date; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; /** * S3 存储协议 所有兼容S3协议的云厂商均支持 * 阿里云 腾讯云 七牛云 minio * - * @author Lion Li + * @author AprilWind */ public class OssClient { + /** + * 服务商 + */ private final String configKey; + /** + * 配置属性 + */ private final OssProperties properties; - private final AmazonS3 client; + /** + * Amazon S3 异步客户端 + */ + private final S3AsyncClient client; + /** + * 用于管理 S3 数据传输的高级工具 + */ + private final S3TransferManager transferManager; + + /** + * AWS S3 预签名 URL 的生成器 + */ + private final S3Presigner presigner; + + /** + * 构造方法 + * + * @param configKey 配置键 + * @param ossProperties Oss配置属性 + */ public OssClient(String configKey, OssProperties ossProperties) { this.configKey = configKey; this.properties = ossProperties; try { - AwsClientBuilder.EndpointConfiguration endpointConfig = - new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion()); + // 创建 AWS 认证信息 + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())); - AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey()); - AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials); - ClientConfiguration clientConfig = new ClientConfiguration(); - if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) { - clientConfig.setProtocol(Protocol.HTTPS); - } else { - clientConfig.setProtocol(Protocol.HTTP); - } - AmazonS3ClientBuilder build = AmazonS3Client.builder() - .withEndpointConfiguration(endpointConfig) - .withClientConfiguration(clientConfig) - .withCredentials(credentialsProvider) - .disableChunkedEncoding(); - if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) { + //创建AWS基于 CRT 的 S3 客户端 + this.client = S3AsyncClient.crtBuilder() + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getEndpoint())) + .region(of()) + .targetThroughputInGbps(20.0) + .minimumPartSizeInBytes(10 * 1025 * 1024L) + .checksumValidationEnabled(false) + .build(); + + //AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端 + this.transferManager = S3TransferManager.builder().s3Client(this.client).build(); + + // 检查是否连接到 MinIO,MinIO 使用 HTTPS 限制使用域名访问,需要启用路径样式访问 + S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false) // minio 使用https限制使用域名访问 需要此配置 站点填域名 - build.enablePathStyleAccess(); - } - this.client = build.build(); + .pathStyleAccessEnabled(!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)).build(); + // 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL + this.presigner = S3Presigner.builder() + .region(of()) + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getDomain())) + .serviceConfiguration(config) + .build(); + + // 创建存储桶 createBucket(); } catch (Exception e) { if (e instanceof OssException) { @@ -78,126 +119,158 @@ public class OssClient { } } + /** + * 同步创建存储桶 + * 如果存储桶不存在,会进行创建;如果存储桶存在,不执行任何操作 + * + * @throws OssException 当创建存储桶时发生异常时抛出 + */ public void createBucket() { + String bucketName = properties.getBucketName(); try { - String bucketName = properties.getBucketName(); - if (client.doesBucketExistV2(bucketName)) { - return; + // 尝试获取存储桶的信息 + client.headBucket( + x -> x.bucket(bucketName) + .build()) + .join(); + } catch (Exception ex) { + if (ex.getCause() instanceof NoSuchBucketException) { + try { + // 存储桶不存在,尝试创建存储桶 + client.createBucket( + x -> x.bucket(bucketName)) + .join(); + + // 设置存储桶的访问策略(Bucket Policy) + client.putBucketPolicy( + x -> x.bucket(bucketName) + .policy(getPolicy(bucketName, getAccessPolicy().getPolicyType()))) + .join(); + } catch (S3Exception e) { + // 存储桶创建或策略设置失败 + throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); + } + } else { + throw new OssException("判断Bucket是否存在失败,请核对配置信息:[" + ex.getMessage() + "]"); } - CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName); - AccessPolicyType accessPolicy = getAccessPolicy(); - createBucketRequest.setCannedAcl(accessPolicy.getAcl()); - client.createBucket(createBucketRequest); - client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType())); - } catch (Exception e) { - throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); } } - public UploadResult upload(byte[] data, String path, String contentType) { - return upload(new ByteArrayInputStream(data), path, contentType); + /** + * 上传文件到 Amazon S3,并返回上传结果 + * + * @param filePath 本地文件路径 + * @param key 在 Amazon S3 中的对象键 + * @param md5Digest 本地文件的 MD5 哈希值(可选) + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult upload(Path filePath, String key, String md5Digest) { + try { + // 构建上传请求对象 + FileUpload fileUpload = transferManager.uploadFile( + x -> x.putObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(key) + .contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null) + .build()) + .addTransferListener(LoggingTransferListener.create()) + .source(filePath).build()); + + // 等待上传完成并获取上传结果 + CompletedFileUpload uploadResult = fileUpload.completionFuture().join(); + String eTag = uploadResult.response().eTag(); + + // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 + return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build(); + } catch (Exception e) { + // 捕获异常并抛出自定义异常 + throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); + } finally { + // 无论上传是否成功,最终都会删除临时文件 + FileUtils.del(filePath); + } } - public UploadResult upload(InputStream inputStream, String path, String contentType) { + /** + * 上传 InputStream 到 Amazon S3 + * + * @param inputStream 要上传的输入流 + * @param key 在 Amazon S3 中的对象键 + * @param length 输入流的长度 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult upload(InputStream inputStream, String key, Long length) { + // 如果输入流不是 ByteArrayInputStream,则将其读取为字节数组再创建 ByteArrayInputStream if (!(inputStream instanceof ByteArrayInputStream)) { inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); } try { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(inputStream.available()); - PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata); - // 设置上传对象的 Acl 为公共读 - putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); - client.putObject(putObjectRequest); + // 创建异步请求体(length如果为空会报错) + BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length); + + // 使用 transferManager 进行上传 + Upload upload = transferManager.upload( + x -> x.requestBody(body) + .putObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(key) + .build()) + .build()); + + // 将输入流写入请求体 + body.writeInputStream(inputStream); + + // 等待文件上传操作完成 + CompletedUpload uploadResult = upload.completionFuture().join(); + String eTag = uploadResult.response().eTag(); + + // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 + return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build(); } catch (Exception e) { throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); } - return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build(); - } - - public UploadResult upload(File file, String path) { - try { - PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file); - // 设置上传对象的 Acl 为公共读 - putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); - client.putObject(putObjectRequest); - } catch (Exception e) { - throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); - } - return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build(); - } - - public void delete(String path) { - path = path.replace(getUrl() + "/", ""); - try { - client.deleteObject(properties.getBucketName(), path); - } catch (Exception e) { - throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]"); - } - } - - public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) { - return upload(data, getPath(properties.getPrefix(), suffix), contentType); - } - - public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) { - return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType); - } - - public UploadResult uploadSuffix(File file, String suffix) { - return upload(file, getPath(properties.getPrefix(), suffix)); } /** - * 获取文件元数据 + * 下载文件从 Amazon S3 到临时目录 * - * @param path 完整文件路径 + * @param path 文件在 Amazon S3 中的对象键 + * @return 下载后的文件在本地的临时路径 + * @throws OssException 如果下载失败,抛出自定义异常 */ - public ObjectMetadata getObjectMetadata(String path) { - path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectMetadata(); + public Path fileDownload(String path) { + // 构建临时文件 + Path tempFilePath = FileUtils.createTempFile().toPath(); + // 使用 S3TransferManager 下载文件 + FileDownload downloadFile = transferManager.downloadFile( + x -> x.getObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(removeBaseUrl(path)) + .build()) + .addTransferListener(LoggingTransferListener.create()) + .destination(tempFilePath) + .build()); + // 等待文件下载操作完成 + downloadFile.completionFuture().join(); + return tempFilePath; } - public InputStream getObjectContent(String path) { - path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectContent(); - } - - public String getUrl() { - String domain = properties.getDomain(); - String endpoint = properties.getEndpoint(); - String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://"; - // 云服务商直接返回 - if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { - if (StringUtils.isNotBlank(domain)) { - return header + domain; - } - return header + properties.getBucketName() + "." + endpoint; + /** + * 删除云存储服务中指定路径下文件 + * + * @param path 指定路径 + */ + public void delete(String path) { + try { + client.deleteObject( + x -> x.bucket(properties.getBucketName()) + .key(removeBaseUrl(path)) + .build()); + } catch (Exception e) { + throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]"); } - // minio 单独处理 - if (StringUtils.isNotBlank(domain)) { - return header + domain + "/" + properties.getBucketName(); - } - return header + endpoint + "/" + properties.getBucketName(); - } - - public String getPath(String prefix, String suffix) { - // 生成uuid - String uuid = IdUtil.fastSimpleUUID(); - // 文件路径 - String path = DateUtils.datePath() + "/" + uuid; - if (StringUtils.isNotBlank(prefix)) { - path = prefix + "/" + path; - } - return path + suffix; - } - - - public String getConfigKey() { - return configKey; } /** @@ -207,14 +280,189 @@ public class OssClient { * @param second 授权时间 */ public String getPrivateUrl(String objectKey, Integer second) { - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey) - .withMethod(HttpMethod.GET) - .withExpiration(new Date(System.currentTimeMillis() + 1000L * second)); - URL url = client.generatePresignedUrl(generatePresignedUrlRequest); + // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL + URL url = presigner.presignGetObject( + x -> x.signatureDuration(Duration.ofSeconds(second)) + .getObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(objectKey) + .build()) + .build()) + .url(); return url.toString(); } + /** + * 上传 byte[] 数据到 Amazon S3,使用指定的后缀构造对象键。 + * + * @param data 要上传的 byte[] 数据 + * @param suffix 对象键的后缀 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult uploadSuffix(byte[] data, String suffix) { + return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length)); + } + + /** + * 上传 InputStream 到 Amazon S3,使用指定的后缀构造对象键。 + * + * @param inputStream 要上传的输入流 + * @param suffix 对象键的后缀 + * @param length 输入流的长度 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length) { + return upload(inputStream, getPath(properties.getPrefix(), suffix), length); + } + + /** + * 上传文件到 Amazon S3,使用指定的后缀构造对象键 + * + * @param file 要上传的文件 + * @param suffix 对象键的后缀 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult uploadSuffix(File file, String suffix) { + return upload(file.toPath(), getPath(properties.getPrefix(), suffix), null); + } + + /** + * 获取文件输入流 + * + * @param path 完整文件路径 + * @return 输入流 + */ + public InputStream getObjectContent(String path) throws IOException { + // 下载文件到临时目录 + Path tempFilePath = fileDownload(path); + // 创建输入流 + InputStream inputStream = Files.newInputStream(tempFilePath); + // 删除临时文件 + FileUtils.del(tempFilePath); + // 返回对象内容的输入流 + return inputStream; + } + + /** + * 获取 S3 客户端的终端点 URL + * + * @return 终端点 URL + */ + public String getEndpoint() { + // 根据配置文件中的是否使用 HTTPS,设置协议头部 + String header = getIsHttps(); + // 拼接协议头部和终端点,得到完整的终端点 URL + return header + properties.getEndpoint(); + } + + /** + * 获取 S3 客户端的终端点 URL(自定义域名) + * + * @return 终端点 URL + */ + public String getDomain() { + // 从配置中获取域名、终端点、是否使用 HTTPS 等信息 + String domain = properties.getDomain(); + String endpoint = properties.getEndpoint(); + String header = getIsHttps(); + + // 如果是云服务商,直接返回域名或终端点 + if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { + return StringUtils.isNotEmpty(domain) ? header + domain : header + endpoint; + } + + // 如果是 MinIO,处理域名并返回 + if (StringUtils.isNotEmpty(domain)) { + return domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP) ? domain : header + domain; + } + + // 返回终端点 + return header + endpoint; + } + + /** + * 根据传入的 region 参数返回相应的 AWS 区域 + * 如果 region 参数非空,使用 Region.of 方法创建并返回对应的 AWS 区域对象 + * 如果 region 参数为空,返回一个默认的 AWS 区域(例如,us-east-1),作为广泛支持的区域 + * + * @return 对应的 AWS 区域对象,或者默认的广泛支持的区域(us-east-1) + */ + public Region of() { + //AWS 区域字符串 + String region = properties.getRegion(); + // 如果 region 参数非空,使用 Region.of 方法创建对应的 AWS 区域对象,否则返回默认区域 + return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1; + } + + /** + * 获取云存储服务的URL + * + * @return 文件路径 + */ + public String getUrl() { + String domain = properties.getDomain(); + String endpoint = properties.getEndpoint(); + String header = getIsHttps(); + // 云服务商直接返回 + if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { + return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint); + } + // MinIO 单独处理 + if (StringUtils.isNotEmpty(domain)) { + // 如果 domain 以 "https://" 或 "http://" 开头 + return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP)) ? + domain + StringUtils.SLASH + properties.getBucketName() : header + domain + StringUtils.SLASH + properties.getBucketName(); + } + return header + endpoint + StringUtils.SLASH + properties.getBucketName(); + } + + /** + * 生成一个符合特定规则的、唯一的文件路径。通过使用日期、UUID、前缀和后缀等元素的组合,确保了文件路径的独一无二性 + * + * @param prefix 前缀 + * @param suffix 后缀 + * @return 文件路径 + */ + public String getPath(String prefix, String suffix) { + // 生成uuid + String uuid = IdUtil.fastSimpleUUID(); + // 生成日期路径 + String datePath = DateUtils.datePath(); + // 拼接路径 + String path = StringUtils.isNotEmpty(prefix) ? + prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid; + return path + suffix; + } + + /** + * 移除路径中的基础URL部分,得到相对路径 + * + * @param path 完整的路径,包括基础URL和相对路径 + * @return 去除基础URL后的相对路径 + */ + public String removeBaseUrl(String path) { + return path.replace(getUrl() + StringUtils.SLASH, ""); + } + + /** + * 服务商 + */ + public String getConfigKey() { + return configKey; + } + + /** + * 获取是否使用 HTTPS 的配置,并返回相应的协议头部。 + * + * @return 协议头部,根据是否使用 HTTPS 返回 "https://" 或 "http://" + */ + public String getIsHttps() { + return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP; + } + /** * 检查配置是否相同 */ @@ -231,32 +479,77 @@ public class OssClient { return AccessPolicyType.getByType(properties.getAccessPolicy()); } + /** + * 生成 AWS S3 存储桶访问策略 + * + * @param bucketName 存储桶 + * @param policyType 桶策略类型 + * @return 符合 AWS S3 存储桶访问策略格式的字符串 + */ private static String getPolicy(String bucketName, PolicyType policyType) { - StringBuilder builder = new StringBuilder(); - builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n"); - builder.append(switch (policyType) { - case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n"; - case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n"; - default -> "\"s3:GetBucketLocation\"\n"; - }); - builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - if (policyType == PolicyType.READ) { - builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - } - builder.append("{\n\"Action\": "); - builder.append(switch (policyType) { - case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; - case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; - default -> "\"s3:GetObject\",\n"; - }); - builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n"); - return builder.toString(); + String policy = switch (policyType) { + case WRITE -> """ + { + "Version": "2012-10-17", + "Statement": [] + } + """; + case READ_WRITE -> """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "s3:GetBucketLocation", + "s3:ListBucket", + "s3:ListBucketMultipartUploads" + ], + "Resource": "arn:aws:s3:::bucketName" + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "s3:AbortMultipartUpload", + "s3:DeleteObject", + "s3:GetObject", + "s3:ListMultipartUploadParts", + "s3:PutObject" + ], + "Resource": "arn:aws:s3:::bucketName/*" + } + ] + } + """; + case READ -> """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetBucketLocation"], + "Resource": "arn:aws:s3:::bucketName" + }, + { + "Effect": "Deny", + "Principal": "*", + "Action": ["s3:ListBucket"], + "Resource": "arn:aws:s3:::bucketName" + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::bucketName/*" + } + ] + } + """; + }; + return policy.replaceAll("bucketName", bucketName); } } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java index a6f57e5ad..81a18e62a 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java @@ -21,4 +21,10 @@ public class UploadResult { * 文件名 */ private String filename; + + /** + * 已上传对象的实体标记(用来校验文件) + */ + private String eTag; + } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java index 9074d72de..6d39133d5 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java @@ -1,8 +1,9 @@ package org.dromara.common.oss.enumd; -import com.amazonaws.services.s3.model.CannedAccessControlList; import lombok.AllArgsConstructor; import lombok.Getter; +import software.amazon.awssdk.services.s3.model.BucketCannedACL; +import software.amazon.awssdk.services.s3.model.ObjectCannedACL; /** * 桶访问策略配置 @@ -16,27 +17,32 @@ public enum AccessPolicyType { /** * private */ - PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE), + PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE), /** * public */ - PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ), + PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE), /** * custom */ - CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ); + CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ); /** - * 桶 权限类型 + * 桶 权限类型(数据库值) */ private final String type; + /** + * 桶 权限类型 + */ + private final BucketCannedACL bucketCannedACL; + /** * 文件对象 权限类型 */ - private final CannedAccessControlList acl; + private final ObjectCannedACL objectCannedACL; /** * 桶策略类型 diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java index 763b090e3..d70270a15 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java @@ -1,5 +1,6 @@ package org.dromara.common.oss.factory; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.CacheNames; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.json.utils.JsonUtils; @@ -9,10 +10,10 @@ import org.dromara.common.oss.exception.OssException; import org.dromara.common.oss.properties.OssProperties; import org.dromara.common.redis.utils.CacheUtils; import org.dromara.common.redis.utils.RedisUtils; -import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; /** * 文件上传Factory @@ -23,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap; public class OssFactory { private static final Map CLIENT_CACHE = new ConcurrentHashMap<>(); + private static final ReentrantLock LOCK = new ReentrantLock(); /** * 获取默认实例 @@ -39,7 +41,7 @@ public class OssFactory { /** * 根据类型获取实例 */ - public static synchronized OssClient instance(String configKey) { + public static OssClient instance(String configKey) { String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey); if (json == null) { throw new OssException("系统异常, '" + configKey + "'配置信息不存在!"); @@ -48,16 +50,19 @@ public class OssFactory { // 使用租户标识避免多个租户相同key实例覆盖 String key = properties.getTenantId() + ":" + configKey; OssClient client = CLIENT_CACHE.get(key); - if (client == null) { - CLIENT_CACHE.put(key, new OssClient(configKey, properties)); - log.info("创建OSS实例 key => {}", configKey); - return CLIENT_CACHE.get(key); - } - // 配置不相同则重新构建 - if (!client.checkPropertiesSame(properties)) { - CLIENT_CACHE.put(key, new OssClient(configKey, properties)); - log.info("重载OSS实例 key => {}", configKey); - return CLIENT_CACHE.get(key); + // 客户端不存在或配置不相同则重新构建 + if (client == null || !client.checkPropertiesSame(properties)) { + LOCK.lock(); + try { + client = CLIENT_CACHE.get(key); + if (client == null || !client.checkPropertiesSame(properties)) { + CLIENT_CACHE.put(key, new OssClient(configKey, properties)); + log.info("创建OSS实例 key => {}", configKey); + return CLIENT_CACHE.get(key); + } + } finally { + LOCK.unlock(); + } } return client; } diff --git a/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java index 8f3a5cafb..02735b073 100644 --- a/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java +++ b/ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java @@ -1,29 +1,29 @@ package org.dromara.common.ratelimiter.aspectj; -import cn.hutool.core.util.ArrayUtil; -import org.dromara.common.core.constant.GlobalConstants; -import org.dromara.common.core.exception.ServiceException; -import org.dromara.common.core.utils.MessageUtils; -import org.dromara.common.core.utils.ServletUtils; -import org.dromara.common.core.utils.StringUtils; -import org.dromara.common.ratelimiter.annotation.RateLimiter; -import org.dromara.common.ratelimiter.enums.LimitType; -import org.dromara.common.redis.utils.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; +import org.dromara.common.core.constant.GlobalConstants; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.MessageUtils; +import org.dromara.common.core.utils.ServletUtils; +import org.dromara.common.core.utils.SpringUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.ratelimiter.annotation.RateLimiter; +import org.dromara.common.ratelimiter.enums.LimitType; +import org.dromara.common.redis.utils.RedisUtils; import org.redisson.api.RateType; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; import java.lang.reflect.Method; @@ -44,21 +44,18 @@ public class RateLimiterAspect { * 定义spel解析模版 */ private final ParserContext parserContext = new TemplateParserContext(); - /** - * 定义spel上下文对象进行解析 - */ - private final EvaluationContext context = new StandardEvaluationContext(); /** * 方法参数解析器 */ private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer(); + @Before("@annotation(rateLimiter)") - public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { + public void doBefore(JoinPoint point, RateLimiter rateLimiter) { int time = rateLimiter.time(); int count = rateLimiter.count(); - String combineKey = getCombineKey(rateLimiter, point); try { + String combineKey = getCombineKey(rateLimiter, point); RateType rateType = RateType.OVERALL; if (rateLimiter.limitType() == LimitType.CLUSTER) { rateType = RateType.PER_CLIENT; @@ -76,42 +73,29 @@ public class RateLimiterAspect { if (e instanceof ServiceException) { throw e; } else { - throw new RuntimeException("服务器限流异常,请稍候再试"); + throw new RuntimeException("服务器限流异常,请稍候再试", e); } } } - public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { + private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { String key = rateLimiter.key(); - // 获取方法(通过方法签名来获取) - MethodSignature signature = (MethodSignature) point.getSignature(); - Method method = signature.getMethod(); - Class targetClass = method.getDeclaringClass(); - // 判断是否是spel格式 - if (StringUtils.containsAny(key, "#")) { - // 获取参数值 + if (StringUtils.isNotBlank(key)) { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method targetMethod = signature.getMethod(); Object[] args = point.getArgs(); - // 获取方法上参数的名称 - String[] parameterNames = pnd.getParameterNames(method); - if (ArrayUtil.isEmpty(parameterNames)) { - throw new ServiceException("限流key解析异常!请联系管理员!"); - } - for (int i = 0; i < parameterNames.length; i++) { - context.setVariable(parameterNames[i], args[i]); - } - // 解析返回给key - try { - Expression expression; - if (StringUtils.startsWith(key, parserContext.getExpressionPrefix()) - && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) { - expression = parser.parseExpression(key, parserContext); - } else { - expression = parser.parseExpression(key); - } - key = expression.getValue(context, String.class) + ":"; - } catch (Exception e) { - throw new ServiceException("限流key解析异常!请联系管理员!"); + //noinspection DataFlowIssue + MethodBasedEvaluationContext context = + new MethodBasedEvaluationContext(null, targetMethod, args, pnd); + context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory())); + Expression expression; + if (StringUtils.startsWith(key, parserContext.getExpressionPrefix()) + && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) { + expression = parser.parseExpression(key, parserContext); + } else { + expression = parser.parseExpression(key); } + key = expression.getValue(context, String.class); } StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY); stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":"); diff --git a/ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/spel-extension.json b/ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/spel-extension.json new file mode 100644 index 000000000..b16432b29 --- /dev/null +++ b/ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/spel-extension.json @@ -0,0 +1,7 @@ +{ + "org.dromara.common.ratelimiter.annotation.RateLimiter@key": { + "method": { + "parameters": true + } + } +} diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java index 0833d2b52..f4328f4d4 100644 --- a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java +++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.redis.config.properties.RedissonProperties; import org.dromara.common.redis.handler.KeyPrefixHandler; import org.redisson.client.codec.StringCodec; @@ -16,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.core.task.VirtualThreadTaskExecutor; /** * redis配置 @@ -48,6 +50,9 @@ public class RedisConfig { // 缓存 Lua 脚本 减少网络传输(redisson 大部分的功能都是基于 Lua 脚本实现) .setUseScriptCache(true) .setCodec(codec); + if (SpringUtils.isVirtual()) { + config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-")); + } RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig(); if (ObjectUtil.isNotNull(singleServerConfig)) { // 使用单机模式 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 058dee769..21acfb1d1 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 @@ -101,13 +101,18 @@ public class LoginHelper { return Convert.toLong(getExtra(DEPT_KEY)); } + /** + * 获取当前 Token 的扩展信息 + * + * @param key 键值 + * @return 对应的扩展数据 + */ private static Object getExtra(String key) { try { return StpUtil.getExtra(key); } catch (Exception e) { return null; } - } /** @@ -135,12 +140,17 @@ public class LoginHelper { return UserConstants.SUPER_ADMIN_ID.equals(userId); } + /** + * 是否为超级管理员 + * + * @return 结果 + */ public static boolean isSuperAdmin() { return isSuperAdmin(getUserId()); } /** - * 是否为超级管理员 + * 是否为租户管理员 * * @param rolePermission 角色权限标识组 * @return 结果 @@ -149,10 +159,20 @@ public class LoginHelper { return rolePermission.contains(TenantConstants.TENANT_ADMIN_ROLE_KEY); } + /** + * 是否为租户管理员 + * + * @return 结果 + */ public static boolean isTenantAdmin() { return Convert.toBool(isTenantAdmin(getLoginUser().getRolePermission())); } + /** + * 检查当前用户是否已登录 + * + * @return 结果 + */ public static boolean isLogin() { return getLoginUser() != null; } diff --git a/ruoyi-common/ruoyi-common-sms/pom.xml b/ruoyi-common/ruoyi-common-sms/pom.xml index c50f222ac..932cb9d52 100644 --- a/ruoyi-common/ruoyi-common-sms/pom.xml +++ b/ruoyi-common/ruoyi-common-sms/pom.xml @@ -20,13 +20,12 @@ org.dromara.sms4j sms4j-spring-boot-starter - - - - com.alibaba - fastjson - - + + + + + org.dromara + ruoyi-common-redis diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java index 29f60fc50..1e88407e8 100644 --- a/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java +++ b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java @@ -1,14 +1,24 @@ package org.dromara.common.sms.config; +import org.dromara.common.sms.core.dao.PlusSmsDao; +import org.dromara.sms4j.api.dao.SmsDao; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; /** - * 短信配置类(暂时没用 预留扩展) + * 短信配置类 * - * @author Lion Li - * @version 4.2.0 + * @author Feng */ -@AutoConfiguration +@AutoConfiguration(after = {RedisAutoConfiguration.class}) public class SmsAutoConfiguration { + @Primary + @Bean + public SmsDao smsDao() { + return new PlusSmsDao(); + } + } diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/dao/PlusSmsDao.java b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/dao/PlusSmsDao.java new file mode 100644 index 000000000..91d8d243a --- /dev/null +++ b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/dao/PlusSmsDao.java @@ -0,0 +1,72 @@ +package org.dromara.common.sms.core.dao; + +import org.dromara.common.core.constant.GlobalConstants; +import org.dromara.common.redis.utils.RedisUtils; +import org.dromara.sms4j.api.dao.SmsDao; + +import java.time.Duration; + +/** + * SmsDao缓存配置 (使用框架自带RedisUtils实现 协议统一) + *

主要用于短信重试和拦截的缓存 + * + * @author Feng + */ +public class PlusSmsDao implements SmsDao { + + /** + * 存储 + * + * @param key 键 + * @param value 值 + * @param cacheTime 缓存时间(单位:秒) + */ + @Override + public void set(String key, Object value, long cacheTime) { + RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key, value, Duration.ofSeconds(cacheTime)); + } + + /** + * 存储 + * + * @param key 键 + * @param value 值 + */ + @Override + public void set(String key, Object value) { + RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key, value, true); + } + + /** + * 读取 + * + * @param key 键 + * @return 值 + */ + @Override + public Object get(String key) { + return RedisUtils.getCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key); + } + + /** + * remove + *

根据key移除缓存 + * + * @param key 缓存键 + * @return 被删除的value + * @author :Wind + */ + @Override + public Object remove(String key) { + return RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + key); + } + + /** + * 清空 + */ + @Override + public void clean() { + RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + "sms:"); + } + +} diff --git a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java index 1beb7d042..1487a6a92 100644 --- a/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java +++ b/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java @@ -16,11 +16,6 @@ import java.util.Map; @ConfigurationProperties(prefix = "justauth") public class SocialProperties { - /** - * 是否启用 - */ - private Boolean enabled; - /** * 授权类型 */ diff --git a/ruoyi-common/ruoyi-common-tenant/pom.xml b/ruoyi-common/ruoyi-common-tenant/pom.xml index f94340a91..8e1a6ab1e 100644 --- a/ruoyi-common/ruoyi-common-tenant/pom.xml +++ b/ruoyi-common/ruoyi-common-tenant/pom.xml @@ -19,6 +19,7 @@ org.dromara ruoyi-common-mybatis + true diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java index 282f1bf40..0f7892861 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java @@ -2,9 +2,11 @@ package org.dromara.common.web.config; import io.undertow.server.DefaultByteBufferPool; import io.undertow.websockets.jsr.WebSocketDeploymentInfo; +import org.dromara.common.core.utils.SpringUtils; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.core.task.VirtualThreadTaskExecutor; /** * Undertow 自定义配置 @@ -24,10 +26,12 @@ public class UndertowConfig implements WebServerFactoryCustomizer sendAliyun(String phones, String templateId) { LinkedHashMap map = new LinkedHashMap<>(1); map.put("code", "1234"); - SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA); + SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map); return R.ok(smsResponse); } @@ -52,9 +50,33 @@ public class SmsController { LinkedHashMap map = new LinkedHashMap<>(1); // map.put("2", "测试测试"); map.put("1", "1234"); - SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.TENCENT); + SmsBlend smsBlend = SmsFactory.getSmsBlend("config2"); SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map); return R.ok(smsResponse); } + /** + * 添加黑名单 + * + * @param phone 手机号 + */ + @GetMapping("/addBlacklist") + public R addBlacklist(String phone){ + SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); + smsBlend.joinInBlacklist(phone); + return R.ok(); + } + + /** + * 移除黑名单 + * + * @param phone 手机号 + */ + @GetMapping("/removeBlacklist") + public R removeBlacklist(String phone){ + SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); + smsBlend.removeFromBlacklist(phone); + return R.ok(); + } + } diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/domain/GenTableColumn.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/domain/GenTableColumn.java index 5ce7ad043..ebdb9930c 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/domain/GenTableColumn.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/domain/GenTableColumn.java @@ -42,7 +42,7 @@ public class GenTableColumn extends BaseEntity { /** * 列描述 */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String columnComment; /** @@ -64,43 +64,43 @@ public class GenTableColumn extends BaseEntity { /** * 是否主键(1是) */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String isPk; /** * 是否自增(1是) */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String isIncrement; /** * 是否必填(1是) */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String isRequired; /** * 是否为插入字段(1是) */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String isInsert; /** * 是否编辑字段(1是) */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String isEdit; /** * 是否列表字段(1是) */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String isList; /** * 是否查询字段(1是) */ - @TableField(updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.VARCHAR) + @TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR) private String isQuery; /** diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/controller.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/controller.java.vm index 14177b5df..64389717d 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/controller.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/controller.java.vm @@ -20,7 +20,7 @@ import org.dromara.common.excel.utils.ExcelUtil; import ${packageName}.domain.vo.${ClassName}Vo; import ${packageName}.domain.bo.${ClassName}Bo; import ${packageName}.service.I${ClassName}Service; -#if($table.crud || $table.sub) +#if($table.crud) import org.dromara.common.mybatis.core.page.TableDataInfo; #elseif($table.tree) #end @@ -44,7 +44,7 @@ public class ${ClassName}Controller extends BaseController { */ @SaCheckPermission("${permissionPrefix}:list") @GetMapping("/list") -#if($table.crud || $table.sub) +#if($table.crud) public TableDataInfo<${ClassName}Vo> list(${ClassName}Bo bo, PageQuery pageQuery) { return ${className}Service.queryPageList(bo, pageQuery); } diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/service.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/service.java.vm index d596a0e91..f1fa3d991 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/service.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/service.java.vm @@ -3,7 +3,7 @@ package ${packageName}.service; import ${packageName}.domain.${ClassName}; import ${packageName}.domain.vo.${ClassName}Vo; import ${packageName}.domain.bo.${ClassName}Bo; -#if($table.crud || $table.sub) +#if($table.crud) import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; #end @@ -24,7 +24,7 @@ public interface I${ClassName}Service { */ ${ClassName}Vo queryById(${pkColumn.javaType} ${pkColumn.javaField}); -#if($table.crud || $table.sub) +#if($table.crud) /** * 查询${functionName}列表 */ diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm index 75a9b83a2..d603de120 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -2,7 +2,7 @@ package ${packageName}.service.impl; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; -#if($table.crud || $table.sub) +#if($table.crud) import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -41,7 +41,7 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service { return baseMapper.selectVoById(${pkColumn.javaField}); } -#if($table.crud || $table.sub) +#if($table.crud) /** * 查询${functionName}列表 */ diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysPostController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysPostController.java index b904b9b37..330b3fb52 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysPostController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysPostController.java @@ -113,7 +113,7 @@ public class SysPostController extends BaseController { * * @param postIds 岗位ID串 */ - @SaCheckPermission("system:dept:query") + @SaCheckPermission("system:post:query") @GetMapping("/optionselect") public R> optionselect(@RequestParam(required = false) Long[] postIds) { return R.ok(postService.selectPostByIds(postIds == null ? null : List.of(postIds))); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java index c44f24cb6..e721aff75 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java @@ -214,7 +214,7 @@ public class SysUserController extends BaseController { @SaCheckPermission("system:user:query") @GetMapping("/optionselect") public R> optioselect(@RequestParam(required = false) Long[] userIds, - @RequestParam(required = false) Long deptId) { + @RequestParam(required = false) Long deptId) { return R.ok(userService.selectUserByIds(userIds == null ? null : List.of(userIds), deptId)); } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysDictTypeBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysDictTypeBo.java index 39257f70b..fcc1ac11d 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysDictTypeBo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysDictTypeBo.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Data; import lombok.EqualsAndHashCode; +import org.dromara.common.core.constant.RegexConstants; import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.system.domain.SysDictType; @@ -37,7 +38,7 @@ public class SysDictTypeBo extends BaseEntity { */ @NotBlank(message = "字典类型不能为空") @Size(min = 0, max = 100, message = "字典类型类型长度不能超过{max}个字符") - @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") + @Pattern(regexp = RegexConstants.DICTIONARY_TYPE, message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") private String dictType; /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssConfigService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssConfigService.java index a8bc57b9c..2f6dfc9a1 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssConfigService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssConfigService.java @@ -31,12 +31,11 @@ public interface ISysOssConfigService { */ TableDataInfo queryPageList(SysOssConfigBo bo, PageQuery pageQuery); - /** * 根据新增业务对象插入对象存储配置 * * @param bo 对象存储配置新增业务对象 - * @return + * @return 结果 */ Boolean insertByBo(SysOssConfigBo bo); @@ -44,7 +43,7 @@ public interface ISysOssConfigService { * 根据编辑业务对象修改对象存储配置 * * @param bo 对象存储配置编辑业务对象 - * @return + * @return 结果 */ Boolean updateByBo(SysOssConfigBo bo); @@ -53,7 +52,7 @@ public interface ISysOssConfigService { * * @param ids 主键集合 * @param isValid 是否校验,true-删除前校验,false-不校验 - * @return + * @return 结果 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java index 2dfe01fe9..057c068ce 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java @@ -19,18 +19,62 @@ import java.util.List; */ public interface ISysOssService { + /** + * 查询OSS对象存储列表 + * + * @param sysOss OSS对象存储分页查询对象 + * @param pageQuery 分页查询实体类 + * @return 结果 + */ TableDataInfo queryPageList(SysOssBo sysOss, PageQuery pageQuery); + /** + * 根据一组 ossIds 获取对应的 SysOssVo 列表 + * + * @param ossIds 一组文件在数据库中的唯一标识集合 + * @return 包含 SysOssVo 对象的列表 + */ List listByIds(Collection ossIds); + /** + * 根据 ossId 从缓存或数据库中获取 SysOssVo 对象 + * + * @param ossId 文件在数据库中的唯一标识 + * @return SysOssVo 对象,包含文件信息 + */ SysOssVo getById(Long ossId); + /** + * 上传 MultipartFile 到对象存储服务,并保存文件信息到数据库 + * + * @param file 要上传的 MultipartFile 对象 + * @return 上传成功后的 SysOssVo 对象,包含文件信息 + */ SysOssVo upload(MultipartFile file); + /** + * 上传文件到对象存储服务,并保存文件信息到数据库 + * + * @param file 要上传的文件对象 + * @return 上传成功后的 SysOssVo 对象,包含文件信息 + */ SysOssVo upload(File file); + /** + * 文件下载方法,支持一次性下载完整文件 + * + * @param ossId OSS对象ID + * @param response HttpServletResponse对象,用于设置响应头和向客户端发送文件内容 + */ void download(Long ossId, HttpServletResponse response) throws IOException; + /** + * 删除OSS对象存储 + * + * @param ids OSS对象ID串 + * @param isValid 判断是否需要校验 + * @return 结果 + */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); } 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 74e1231f3..0325a2558 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 @@ -215,8 +215,8 @@ public interface ISysUserService { /** * 通过部门id查询当前部门所有用户 * - * @param deptId - * @return + * @param deptId 部门id + * @return 结果 */ List selectUserListByDept(Long deptId); } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java index 150de00e4..9c930a0c5 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java @@ -5,31 +5,29 @@ import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgentUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.Constants; import org.dromara.common.core.utils.MapstructUtils; -import org.dromara.common.mybatis.core.page.PageQuery; -import org.dromara.common.log.event.LogininforEvent; -import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.core.utils.ServletUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.ip.AddressUtils; +import org.dromara.common.log.event.LogininforEvent; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.satoken.utils.LoginHelper; -import org.dromara.system.domain.SysClient; import org.dromara.system.domain.SysLogininfor; import org.dromara.system.domain.bo.SysLogininforBo; import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysLogininforVo; -import org.dromara.system.mapper.SysClientMapper; import org.dromara.system.mapper.SysLogininforMapper; import org.dromara.system.service.ISysClientService; import org.dromara.system.service.ISysLogininforService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -61,10 +59,10 @@ public class SysLogininforServiceImpl implements ISysLogininforService { final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent")); final String ip = ServletUtils.getClientIP(request); // 客户端信息 - String clientid = request.getHeader(LoginHelper.CLIENT_KEY); + String clientId = request.getHeader(LoginHelper.CLIENT_KEY); SysClientVo client = null; - if (StringUtils.isNotBlank(clientid)) { - client = clientService.queryByClientId(clientid); + if (StringUtils.isNotBlank(clientId)) { + client = clientService.queryByClientId(clientId); } String address = AddressUtils.getRealAddressByIP(ip); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java index c7d47191d..1df221c32 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java @@ -6,6 +6,8 @@ import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.dromara.common.core.constant.CacheNames; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.service.OssService; @@ -25,8 +27,6 @@ import org.dromara.system.domain.bo.SysOssBo; import org.dromara.system.domain.vo.SysOssVo; import org.dromara.system.mapper.SysOssMapper; import org.dromara.system.service.ISysOssService; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.MediaType; @@ -36,7 +36,10 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; /** * 文件上传 服务层实现 @@ -49,6 +52,13 @@ public class SysOssServiceImpl implements ISysOssService, OssService { private final SysOssMapper baseMapper; + /** + * 查询OSS对象存储列表 + * + * @param bo OSS对象存储分页查询对象 + * @param pageQuery 分页查询实体类 + * @return 结果 + */ @Override public TableDataInfo queryPageList(SysOssBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); @@ -58,6 +68,12 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return TableDataInfo.build(result); } + /** + * 根据一组 ossIds 获取对应的 SysOssVo 列表 + * + * @param ossIds 一组文件在数据库中的唯一标识集合 + * @return 包含 SysOssVo 对象的列表 + */ @Override public List listByIds(Collection ossIds) { List list = new ArrayList<>(); @@ -75,6 +91,12 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return list; } + /** + * 根据一组 ossIds 获取对应文件的 URL 列表 + * + * @param ossIds 以逗号分隔的 ossId 字符串 + * @return 以逗号分隔的文件 URL 字符串 + */ @Override public String selectUrlByIds(String ossIds) { List list = new ArrayList<>(); @@ -107,12 +129,25 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return lqw; } + /** + * 根据 ossId 从缓存或数据库中获取 SysOssVo 对象 + * + * @param ossId 文件在数据库中的唯一标识 + * @return SysOssVo 对象,包含文件信息 + */ @Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId") @Override public SysOssVo getById(Long ossId) { return baseMapper.selectVoById(ossId); } + + /** + * 文件下载方法,支持一次性下载完整文件 + * + * @param ossId OSS对象ID + * @param response HttpServletResponse对象,用于设置响应头和向客户端发送文件内容 + */ @Override public void download(Long ossId, HttpServletResponse response) throws IOException { SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId); @@ -122,7 +157,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName()); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); OssClient storage = OssFactory.instance(sysOss.getService()); - try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) { + try (InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) { int available = inputStream.available(); IoUtil.copy(inputStream, response.getOutputStream(), available); response.setContentLength(available); @@ -131,6 +166,13 @@ public class SysOssServiceImpl implements ISysOssService, OssService { } } + /** + * 上传 MultipartFile 到对象存储服务,并保存文件信息到数据库 + * + * @param file 要上传的 MultipartFile 对象 + * @return 上传成功后的 SysOssVo 对象,包含文件信息 + * @throws ServiceException 如果上传过程中发生异常,则抛出 ServiceException 异常 + */ @Override public SysOssVo upload(MultipartFile file) { String originalfileName = file.getOriginalFilename(); @@ -138,7 +180,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { OssClient storage = OssFactory.instance(); UploadResult uploadResult; try { - uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType()); + uploadResult = storage.uploadSuffix(file.getBytes(), suffix); } catch (IOException e) { throw new ServiceException(e.getMessage()); } @@ -146,6 +188,12 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult); } + /** + * 上传文件到对象存储服务,并保存文件信息到数据库 + * + * @param file 要上传的文件对象 + * @return 上传成功后的 SysOssVo 对象,包含文件信息 + */ @Override public SysOssVo upload(File file) { String originalfileName = file.getName(); @@ -169,6 +217,13 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return this.matchingUrl(sysOssVo); } + /** + * 删除OSS对象存储 + * + * @param ids OSS对象ID串 + * @param isValid 判断是否需要校验 + * @return 结果 + */ @Override public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if (isValid) { @@ -183,7 +238,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { } /** - * 匹配Url + * 桶类型为 private 的URL 修改为临时URL时长为120s * * @param oss OSS对象 * @return oss 匹配Url的OSS对象 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 89270326e..a4d553b49 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 @@ -579,4 +579,30 @@ public class SysUserServiceImpl implements ISysUserService, UserService { return ObjectUtil.isNull(sysUser) ? null : sysUser.getNickName(); } + /** + * 通过用户ID查询用户手机号 + * + * @param userId 用户id + * @return 用户手机号 + */ + @Override + public String selectPhonenumberById(Long userId) { + SysUser sysUser = baseMapper.selectOne(new LambdaQueryWrapper() + .select(SysUser::getPhonenumber).eq(SysUser::getUserId, userId)); + return ObjectUtil.isNull(sysUser) ? null : sysUser.getPhonenumber(); + } + + /** + * 通过用户ID查询用户邮箱 + * + * @param userId 用户id + * @return 用户邮箱 + */ + @Override + public String selectEmailById(Long userId) { + SysUser sysUser = baseMapper.selectOne(new LambdaQueryWrapper() + .select(SysUser::getEmail).eq(SysUser::getUserId, userId)); + return ObjectUtil.isNull(sysUser) ? null : sysUser.getEmail(); + } + }