diff --git a/.run/ruoyi-monitor-admin.run.xml b/.run/ruoyi-monitor-admin.run.xml
index 5b32519c5..478b4f30d 100644
--- a/.run/ruoyi-monitor-admin.run.xml
+++ b/.run/ruoyi-monitor-admin.run.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.run/ruoyi-server.run.xml b/.run/ruoyi-server.run.xml
index 9fefae69a..541800ddc 100644
--- a/.run/ruoyi-server.run.xml
+++ b/.run/ruoyi-server.run.xml
@@ -1,12 +1,12 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/.run/ruoyi-snailjob-server.run.xml b/.run/ruoyi-snailjob-server.run.xml
index 914809dbf..5221eef4f 100644
--- a/.run/ruoyi-snailjob-server.run.xml
+++ b/.run/ruoyi-snailjob-server.run.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/README.md b/README.md
index ad9c2cee7..eeb3f7b4b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.2-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@@ -61,6 +61,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow
| 数据库连接池 | 采用 HikariCP Spring官方内置连接池 配置简单 以性能与稳定性闻名天下 | 采用 druid bug众多 社区维护差 活跃度低 配置众多繁琐性能一般 |
| 数据库主键 | 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁 | 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一 |
| WebSocket协议 | 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物 | 无 |
+| SSE推送 | 采用 Spring SSE 实现 扩展了Token鉴权与分布式会话同步 | 无 |
| 序列化 | 采用 Jackson Spring官方内置序列化 靠谱!!! | 采用 fastjson bugjson 远近闻名 |
| 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 |
| 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 |
@@ -72,6 +73,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释
只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
| Excel框架 | 采用 Alibaba EasyExcel 基于插件化
框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等 | 基于 POI 手写实现 功能有限 复杂 扩展性差 |
+| 工作流支持 | 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能 | 无 |
| 工具类框架 | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等 |
| 监控框架 | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制
实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 |
| 链路追踪 | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗
用了它即可实时查看请求经过的每一处每一个节点 | 无 |
diff --git a/pom.xml b/pom.xml
index 7094419ca..750673b87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,45 +13,46 @@
RuoYi-Vue-Plus多租户管理系统
- 5.2.1
- 3.2.6
+ 5.2.2
+ 3.2.9
UTF-8
UTF-8
17
3.5.16
- 2.5.0
+ 2.6.0
0.15.0
- 5.2.3
- 3.3.4
+ 4.0.2
2.3
1.38.0
3.5.7
3.9.1
- 5.8.27
+ 5.8.31
4.10.0
3.2.3
- 3.31.0
+ 3.34.1
2.2.7
4.3.1
- 2.14.4
- 1.0.1
- 1.3.6
+ 1.1.2
+ 1.4.4
0.2.0
- 1.18.32
+ 1.18.34
1.76
1.16.6
2.7.0
+ 2.3.15.Final
2.25.15
0.29.13
- 3.2.1
+ 3.3.2
1.2.83
+
+ 8.7.2-20240808
- 7.0.0
+ 7.0.1
3.2.2
@@ -155,26 +156,10 @@
${lombok.version}
-
- org.apache.poi
- poi
- ${poi.version}
-
-
- org.apache.poi
- poi-ooxml
- ${poi.version}
-
com.alibaba
easyexcel
${easyexcel.version}
-
-
- org.apache.poi
- poi-ooxml-schemas
-
-
@@ -307,12 +292,6 @@
${snailjob.version}
-
- com.alibaba
- transmittable-thread-local
- ${alibaba-ttl.version}
-
-
org.bouncycastle
@@ -333,6 +312,28 @@
${ip2region.version}
+
+ io.undertow
+ undertow-core
+ ${undertow.version}
+
+
+ io.undertow
+ undertow-servlet
+ ${undertow.version}
+
+
+ io.undertow
+ undertow-websockets-jsr
+ ${undertow.version}
+
+
+
+ commons-compress
+ org.apache.commons
+ 1.26.2
+
+
com.alibaba
fastjson
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 610e9d706..9e9780464 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -22,21 +22,28 @@
com.mysql
mysql-connector-j
-
-
- com.oracle.database.jdbc
- ojdbc8
-
-
-
- org.postgresql
- postgresql
-
-
-
- com.microsoft.sqlserver
- mssql-jdbc
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
org.dromara
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 1db68f106..b561693d6 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
@@ -24,9 +24,9 @@
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.sse.dto.SseMessageDto;
+import org.dromara.common.sse.utils.SseMessageUtils;
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.bo.SysTenantBo;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysTenantVo;
@@ -102,11 +102,11 @@ public R login(@RequestBody String body) {
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
- WebSocketMessageDto dto = new WebSocketMessageDto();
+ SseMessageDto dto = new SseMessageDto();
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
- dto.setSessionKeys(List.of(userId));
- WebSocketUtils.publishMessage(dto);
- }, 3, TimeUnit.SECONDS);
+ dto.setUserIds(List.of(userId));
+ SseMessageUtils.publishMessage(dto);
+ }, 5, TimeUnit.SECONDS);
return R.ok(loginVo);
}
diff --git a/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java b/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java
index a4724043b..07595e092 100644
--- a/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java
+++ b/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java
@@ -3,6 +3,8 @@
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.convert.Convert;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.RequiredArgsConstructor;
@@ -81,7 +83,10 @@ public void doLogin(String loginType, Object loginId, String tokenValue, SaLogin
*/
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
- RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+ TenantHelper.dynamic(tenantId, () -> {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ });
log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
}
@@ -90,7 +95,10 @@ public void doLogout(String loginType, Object loginId, String tokenValue) {
*/
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
- RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+ TenantHelper.dynamic(tenantId, () -> {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ });
log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
}
@@ -99,7 +107,10 @@ public void doKickout(String loginType, Object loginId, String tokenValue) {
*/
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
- RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+ TenantHelper.dynamic(tenantId, () -> {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ });
log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
}
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 af6e7f557..c7ad9179c 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
@@ -4,13 +4,14 @@
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.lock.annotation.Lock4j;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
+import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.constant.Constants;
-import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.domain.dto.RoleDTO;
import org.dromara.common.core.domain.model.LoginUser;
@@ -155,16 +156,13 @@ public LoginUser buildLoginUser(SysUserVo user) {
loginUser.setUserType(user.getUserType());
loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
- TenantHelper.dynamic(user.getTenantId(), () -> {
- SysDeptVo dept = null;
- if (ObjectUtil.isNotNull(user.getDeptId())) {
- dept = deptService.selectDeptById(user.getDeptId());
- }
- loginUser.setDeptName(ObjectUtil.isNull(dept) ? "" : dept.getDeptName());
- loginUser.setDeptCategory(ObjectUtil.isNull(dept) ? "" : dept.getDeptCategory());
- List roles = roleService.selectRolesByUserId(user.getUserId());
- loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
- });
+ if (ObjectUtil.isNotNull(user.getDeptId())) {
+ Opt deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
+ loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
+ loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
+ }
+ List roles = roleService.selectRolesByUserId(user.getUserId());
+ loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
return loginUser;
}
@@ -186,7 +184,7 @@ public void recordLoginInfo(Long userId, String ip) {
* 登录校验
*/
public void checkLogin(LoginType loginType, String tenantId, String username, Supplier supplier) {
- String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
+ String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java
index 38fdc448b..b5a24976e 100644
--- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java
+++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java
@@ -21,7 +21,6 @@
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
-import org.dromara.system.domain.SysClient;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
@@ -51,13 +50,12 @@ public LoginVo login(String body, SysClientVo client) {
String tenantId = loginBody.getTenantId();
String email = loginBody.getEmail();
String emailCode = loginBody.getEmailCode();
-
- // 通过邮箱查找用户
- SysUserVo user = loadUserByEmail(tenantId, email);
-
- loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
- // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
- LoginUser loginUser = loginService.buildLoginUser(user);
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+ SysUserVo user = loadUserByEmail(email);
+ loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+ return loginService.buildLoginUser(user);
+ });
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@@ -89,18 +87,16 @@ private boolean validateEmailCode(String tenantId, String email, String emailCod
return code.equals(emailCode);
}
- private SysUserVo loadUserByEmail(String tenantId, String email) {
- return TenantHelper.dynamic(tenantId, () -> {
- SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getEmail, email));
- if (ObjectUtil.isNull(user)) {
- log.info("登录用户:{} 不存在.", email);
- throw new UserException("user.not.exists", email);
- } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
- log.info("登录用户:{} 已被停用.", email);
- throw new UserException("user.blocked", email);
- }
- return user;
- });
+ private SysUserVo loadUserByEmail(String email) {
+ SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getEmail, email));
+ if (ObjectUtil.isNull(user)) {
+ log.info("登录用户:{} 不存在.", email);
+ throw new UserException("user.not.exists", email);
+ } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+ log.info("登录用户:{} 已被停用.", email);
+ throw new UserException("user.blocked", email);
+ }
+ return user;
}
}
diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
index 5d3ebd755..f28024f35 100644
--- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
+++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
@@ -62,11 +62,12 @@ public LoginVo login(String body, SysClientVo client) {
if (captchaEnabled) {
validateCaptcha(tenantId, username, code, uuid);
}
-
- SysUserVo user = loadUserByUsername(tenantId, username);
- loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
- // 此处可根据登录用户的数据不同 自行创建 loginUser
- LoginUser loginUser = loginService.buildLoginUser(user);
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+ SysUserVo user = loadUserByUsername(username);
+ loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
+ // 此处可根据登录用户的数据不同 自行创建 loginUser
+ return loginService.buildLoginUser(user);
+ });
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@@ -107,18 +108,16 @@ private void validateCaptcha(String tenantId, String username, String code, Stri
}
}
- private SysUserVo loadUserByUsername(String tenantId, String username) {
- return TenantHelper.dynamic(tenantId, () -> {
- SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getUserName, username));
- if (ObjectUtil.isNull(user)) {
- log.info("登录用户:{} 不存在.", username);
- throw new UserException("user.not.exists", username);
- } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
- log.info("登录用户:{} 已被停用.", username);
- throw new UserException("user.blocked", username);
- }
- return user;
- });
+ private SysUserVo loadUserByUsername(String username) {
+ SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getUserName, username));
+ if (ObjectUtil.isNull(user)) {
+ log.info("登录用户:{} 不存在.", username);
+ throw new UserException("user.not.exists", username);
+ } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+ log.info("登录用户:{} 已被停用.", username);
+ throw new UserException("user.blocked", username);
+ }
+ return user;
}
}
diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java
index f883632f9..89f846244 100644
--- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java
+++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java
@@ -21,7 +21,6 @@
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
-import org.dromara.system.domain.SysClient;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
@@ -51,13 +50,12 @@ public LoginVo login(String body, SysClientVo client) {
String tenantId = loginBody.getTenantId();
String phonenumber = loginBody.getPhonenumber();
String smsCode = loginBody.getSmsCode();
-
- // 通过手机号查找用户
- SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
-
- loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
- // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
- LoginUser loginUser = loginService.buildLoginUser(user);
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+ SysUserVo user = loadUserByPhonenumber(phonenumber);
+ loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+ return loginService.buildLoginUser(user);
+ });
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@@ -89,18 +87,16 @@ private boolean validateSmsCode(String tenantId, String phonenumber, String smsC
return code.equals(smsCode);
}
- private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
- return TenantHelper.dynamic(tenantId, () -> {
- SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getPhonenumber, phonenumber));
- if (ObjectUtil.isNull(user)) {
- log.info("登录用户:{} 不存在.", phonenumber);
- throw new UserException("user.not.exists", phonenumber);
- } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
- log.info("登录用户:{} 已被停用.", phonenumber);
- throw new UserException("user.blocked", phonenumber);
- }
- return user;
- });
+ private SysUserVo loadUserByPhonenumber(String phonenumber) {
+ SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getPhonenumber, phonenumber));
+ if (ObjectUtil.isNull(user)) {
+ log.info("登录用户:{} 不存在.", phonenumber);
+ throw new UserException("user.not.exists", phonenumber);
+ } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+ log.info("登录用户:{} 已被停用.", phonenumber);
+ throw new UserException("user.blocked", phonenumber);
+ }
+ return user;
}
}
diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
index 01db20027..84630260f 100644
--- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
+++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
@@ -92,11 +92,11 @@ public LoginVo login(String body, SysClientVo client) {
} else {
social = list.get(0);
}
- // 查找用户
- SysUserVo user = loadUser(social.getTenantId(), social.getUserId());
-
- // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
- LoginUser loginUser = loginService.buildLoginUser(user);
+ LoginUser loginUser = TenantHelper.dynamic(social.getTenantId(), () -> {
+ SysUserVo user = loadUser(social.getUserId());
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+ return loginService.buildLoginUser(user);
+ });
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@@ -116,18 +116,16 @@ public LoginVo login(String body, SysClientVo client) {
return loginVo;
}
- private SysUserVo loadUser(String tenantId, Long userId) {
- return TenantHelper.dynamic(tenantId, () -> {
- SysUserVo user = userMapper.selectVoById(userId);
- if (ObjectUtil.isNull(user)) {
- log.info("登录用户:{} 不存在.", "");
- throw new UserException("user.not.exists", "");
- } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
- log.info("登录用户:{} 已被停用.", "");
- throw new UserException("user.blocked", "");
- }
- return user;
- });
+ private SysUserVo loadUser(Long userId) {
+ SysUserVo user = userMapper.selectVoById(userId);
+ if (ObjectUtil.isNull(user)) {
+ log.info("登录用户:{} 不存在.", "");
+ throw new UserException("user.not.exists", "");
+ } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+ log.info("登录用户:{} 已被停用.", "");
+ throw new UserException("user.blocked", "");
+ }
+ return user;
}
}
diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml
index ea5cafac0..5e20daee7 100644
--- a/ruoyi-admin/src/main/resources/application-dev.yml
+++ b/ruoyi-admin/src/main/resources/application-dev.yml
@@ -5,6 +5,9 @@ spring.boot.admin.client:
url: http://localhost:9090/admin
instance:
service-host-type: IP
+ metadata:
+ username: ${spring.boot.admin.client.username}
+ userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index 2a4bc11e9..2823bba11 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -8,6 +8,9 @@ spring.boot.admin.client:
url: http://localhost:9090/admin
instance:
service-host-type: IP
+ metadata:
+ username: ${spring.boot.admin.client.username}
+ userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index 27de286f0..5d94bef93 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -121,9 +121,6 @@ security:
# swagger 文档配置
- /*/api-docs
- /*/api-docs/**
- # actuator 监控配置
- - /actuator
- - /actuator/**
# 多租户配置
tenant:
@@ -259,10 +256,15 @@ management:
logfile:
external-file: ./logs/sys-console.log
+--- # 默认/推荐使用sse推送
+sse:
+ enabled: true
+ path: /resource/sse
+
--- # websocket
websocket:
# 如果关闭 需要和前端开关一起关闭
- enabled: true
+ enabled: false
# 路径
path: /resource/websocket
# 设置访问源地址
@@ -270,6 +272,10 @@ websocket:
--- #flowable配置
flowable:
+ # 开关 用于启动/停用工作流
+ enabled: true
+ process.enabled: ${flowable.enabled}
+ eventregistry.enabled: ${flowable.enabled}
async-executor-activate: false #关闭定时任务JOB
# 将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
database-schema-update: true
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index 45493d3e9..2930fd0b0 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -33,6 +33,7 @@
ruoyi-common-encrypt
ruoyi-common-tenant
ruoyi-common-websocket
+ ruoyi-common-sse
ruoyi-common
diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml
index 5388d8c46..455408d8c 100644
--- a/ruoyi-common/ruoyi-common-bom/pom.xml
+++ b/ruoyi-common/ruoyi-common-bom/pom.xml
@@ -14,7 +14,7 @@
- 5.2.1
+ 5.2.2
@@ -172,6 +172,13 @@
${revision}
+
+
+ org.dromara
+ ruoyi-common-sse
+ ${revision}
+
+
diff --git a/ruoyi-common/ruoyi-common-core/pom.xml b/ruoyi-common/ruoyi-common-core/pom.xml
index 5925c9b3c..ad37e90db 100644
--- a/ruoyi-common/ruoyi-common-core/pom.xml
+++ b/ruoyi-common/ruoyi-common-core/pom.xml
@@ -94,11 +94,6 @@
ip2region
-
- com.alibaba
- transmittable-thread-local
-
-
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
index 67bc8e4c2..ceb837044 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
@@ -22,4 +22,9 @@ public interface CacheConstants {
*/
String SYS_DICT_KEY = "sys_dict:";
+ /**
+ * 登录账户密码错误次数 redis key
+ */
+ String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+
}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
index ae9bc2e62..5352b118f 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
@@ -27,11 +27,6 @@ public interface GlobalConstants {
*/
String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
- /**
- * 登录账户密码错误次数 redis key
- */
- String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
-
/**
* 三方认证 redis key
*/
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
index 6f3b0b96b..76f6dd4ad 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
@@ -67,6 +67,16 @@ public interface UserConstants {
*/
String DICT_NORMAL = "0";
+ /**
+ * 通用存在标志
+ */
+ String DEL_FLAG_NORMAL = "0";
+
+ /**
+ * 通用删除标志
+ */
+ String DEL_FLAG_REMOVED = "2";
+
/**
* 是否为系统默认(是)
*/
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 0f2878da4..43aef28cb 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
@@ -66,4 +66,20 @@ public interface UserService {
* @return 用户ids
*/
List selectUserIdsByRoleIds(List roleIds);
+
+ /**
+ * 通过角色ID查询用户
+ *
+ * @param roleIds 角色ids
+ * @return 用户
+ */
+ List selectUsersByRoleIds(List roleIds);
+
+ /**
+ * 通过部门ID查询用户
+ *
+ * @param deptIds 部门ids
+ * @return 用户
+ */
+ List selectUsersByDeptIds(List deptIds);
}
diff --git a/ruoyi-common/ruoyi-common-excel/pom.xml b/ruoyi-common/ruoyi-common-excel/pom.xml
index dd4a5eebe..14b9410bb 100644
--- a/ruoyi-common/ruoyi-common-excel/pom.xml
+++ b/ruoyi-common/ruoyi-common-excel/pom.xml
@@ -25,6 +25,11 @@
com.alibaba
easyexcel
+
+ commons-compress
+ org.apache.commons
+ 1.26.2
+
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 7c0a48b9a..7c7721c60 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
@@ -107,7 +107,7 @@ private List handle(List> list, boolean hasTitle) {
}
if (!cellValue.equals(val)) {
- if ((i - repeatCell.getCurrent() > 1) && isMerge(list, i, field)) {
+ if ((i - repeatCell.getCurrent() > 1)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
}
map.put(field, new RepeatCell(val, i));
@@ -115,6 +115,11 @@ private List handle(List> list, boolean hasTitle) {
if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
}
+ } else if (!isMerge(list, i, field)) {
+ if ((i - repeatCell.getCurrent() > 1)) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
+ }
+ map.put(field, new RepeatCell(val, i));
}
}
}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java
index 1b51c272c..0ea3007b9 100644
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java
@@ -1,7 +1,7 @@
package org.dromara.common.mail.config;
+import cn.hutool.extra.mail.MailAccount;
import org.dromara.common.mail.config.properties.MailProperties;
-import org.dromara.common.mail.utils.MailAccount;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java
deleted file mode 100644
index fdae86975..000000000
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.io.IORuntimeException;
-
-/**
- * 全局邮件帐户,依赖于邮件配置文件{@link MailAccount#MAIL_SETTING_PATHS}
- *
- * @author looly
- */
-public enum GlobalMailAccount {
- INSTANCE;
-
- private final MailAccount mailAccount;
-
- /**
- * 构造
- */
- GlobalMailAccount() {
- mailAccount = createDefaultAccount();
- }
-
- /**
- * 获得邮件帐户
- *
- * @return 邮件帐户
- */
- public MailAccount getAccount() {
- return this.mailAccount;
- }
-
- /**
- * 创建默认帐户
- *
- * @return MailAccount
- */
- private MailAccount createDefaultAccount() {
- for (String mailSettingPath : MailAccount.MAIL_SETTING_PATHS) {
- try {
- return new MailAccount(mailSettingPath);
- } catch (IORuntimeException ignore) {
- //ignore
- }
- }
- return null;
- }
-}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java
deleted file mode 100644
index b755e7370..000000000
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.util.ArrayUtil;
-import jakarta.mail.internet.AddressException;
-import jakarta.mail.internet.InternetAddress;
-import jakarta.mail.internet.MimeUtility;
-
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * 邮件内部工具类
- *
- * @author looly
- * @since 3.2.3
- */
-public class InternalMailUtil {
-
- /**
- * 将多个字符串邮件地址转为{@link InternetAddress}列表
- * 单个字符串地址可以是多个地址合并的字符串
- *
- * @param addrStrs 地址数组
- * @param charset 编码(主要用于中文用户名的编码)
- * @return 地址数组
- * @since 4.0.3
- */
- public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {
- final List resultList = new ArrayList<>(addrStrs.length);
- InternetAddress[] addrs;
- for (String addrStr : addrStrs) {
- addrs = parseAddress(addrStr, charset);
- if (ArrayUtil.isNotEmpty(addrs)) {
- Collections.addAll(resultList, addrs);
- }
- }
- return resultList.toArray(new InternetAddress[0]);
- }
-
- /**
- * 解析第一个地址
- *
- * @param address 地址字符串
- * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
- * @return 地址列表
- */
- public static InternetAddress parseFirstAddress(String address, Charset charset) {
- final InternetAddress[] internetAddresses = parseAddress(address, charset);
- if (ArrayUtil.isEmpty(internetAddresses)) {
- try {
- return new InternetAddress(address);
- } catch (AddressException e) {
- throw new MailException(e);
- }
- }
- return internetAddresses[0];
- }
-
- /**
- * 将一个地址字符串解析为多个地址
- * 地址间使用" "、","、";"分隔
- *
- * @param address 地址字符串
- * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
- * @return 地址列表
- */
- public static InternetAddress[] parseAddress(String address, Charset charset) {
- InternetAddress[] addresses;
- try {
- addresses = InternetAddress.parse(address);
- } catch (AddressException e) {
- throw new MailException(e);
- }
- //编码用户名
- if (ArrayUtil.isNotEmpty(addresses)) {
- final String charsetStr = null == charset ? null : charset.name();
- for (InternetAddress internetAddress : addresses) {
- try {
- internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);
- } catch (UnsupportedEncodingException e) {
- throw new MailException(e);
- }
- }
- }
-
- return addresses;
- }
-
- /**
- * 编码中文字符
- * 编码失败返回原字符串
- *
- * @param text 被编码的文本
- * @param charset 编码
- * @return 编码后的结果
- */
- public static String encodeText(String text, Charset charset) {
- try {
- return MimeUtility.encodeText(text, charset.name(), null);
- } catch (UnsupportedEncodingException e) {
- // ignore
- }
- return text;
- }
-}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java
deleted file mode 100644
index 6ca4b69ec..000000000
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java
+++ /dev/null
@@ -1,483 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.builder.Builder;
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.IORuntimeException;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
-import jakarta.activation.DataHandler;
-import jakarta.activation.DataSource;
-import jakarta.activation.FileDataSource;
-import jakarta.activation.FileTypeMap;
-import jakarta.mail.*;
-import jakarta.mail.internet.MimeBodyPart;
-import jakarta.mail.internet.MimeMessage;
-import jakarta.mail.internet.MimeMultipart;
-import jakarta.mail.internet.MimeUtility;
-import jakarta.mail.util.ByteArrayDataSource;
-
-import java.io.*;
-import java.nio.charset.Charset;
-import java.util.Date;
-
-/**
- * 邮件发送客户端
- *
- * @author looly
- * @since 3.2.0
- */
-public class Mail implements Builder {
- @Serial
- private static final long serialVersionUID = 1L;
-
- /**
- * 邮箱帐户信息以及一些客户端配置信息
- */
- private final MailAccount mailAccount;
- /**
- * 收件人列表
- */
- private String[] tos;
- /**
- * 抄送人列表(carbon copy)
- */
- private String[] ccs;
- /**
- * 密送人列表(blind carbon copy)
- */
- private String[] bccs;
- /**
- * 回复地址(reply-to)
- */
- private String[] reply;
- /**
- * 标题
- */
- private String title;
- /**
- * 内容
- */
- private String content;
- /**
- * 是否为HTML
- */
- private boolean isHtml;
- /**
- * 正文、附件和图片的混合部分
- */
- private final Multipart multipart = new MimeMultipart();
- /**
- * 是否使用全局会话,默认为false
- */
- private boolean useGlobalSession = false;
-
- /**
- * debug输出位置,可以自定义debug日志
- */
- private PrintStream debugOutput;
-
- /**
- * 创建邮件客户端
- *
- * @param mailAccount 邮件帐号
- * @return Mail
- */
- public static Mail create(MailAccount mailAccount) {
- return new Mail(mailAccount);
- }
-
- /**
- * 创建邮件客户端,使用全局邮件帐户
- *
- * @return Mail
- */
- public static Mail create() {
- return new Mail();
- }
-
- // --------------------------------------------------------------- Constructor start
-
- /**
- * 构造,使用全局邮件帐户
- */
- public Mail() {
- this(GlobalMailAccount.INSTANCE.getAccount());
- }
-
- /**
- * 构造
- *
- * @param mailAccount 邮件帐户,如果为null使用默认配置文件的全局邮件配置
- */
- public Mail(MailAccount mailAccount) {
- mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
- this.mailAccount = mailAccount.defaultIfEmpty();
- }
- // --------------------------------------------------------------- Constructor end
-
- // --------------------------------------------------------------- Getters and Setters start
-
- /**
- * 设置收件人
- *
- * @param tos 收件人列表
- * @return this
- * @see #setTos(String...)
- */
- public Mail to(String... tos) {
- return setTos(tos);
- }
-
- /**
- * 设置多个收件人
- *
- * @param tos 收件人列表
- * @return this
- */
- public Mail setTos(String... tos) {
- this.tos = tos;
- return this;
- }
-
- /**
- * 设置多个抄送人(carbon copy)
- *
- * @param ccs 抄送人列表
- * @return this
- * @since 4.0.3
- */
- public Mail setCcs(String... ccs) {
- this.ccs = ccs;
- return this;
- }
-
- /**
- * 设置多个密送人(blind carbon copy)
- *
- * @param bccs 密送人列表
- * @return this
- * @since 4.0.3
- */
- public Mail setBccs(String... bccs) {
- this.bccs = bccs;
- return this;
- }
-
- /**
- * 设置多个回复地址(reply-to)
- *
- * @param reply 回复地址(reply-to)列表
- * @return this
- * @since 4.6.0
- */
- public Mail setReply(String... reply) {
- this.reply = reply;
- return this;
- }
-
- /**
- * 设置标题
- *
- * @param title 标题
- * @return this
- */
- public Mail setTitle(String title) {
- this.title = title;
- return this;
- }
-
- /**
- * 设置正文
- * 正文可以是普通文本也可以是HTML(默认普通文本),可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
- *
- * @param content 正文
- * @return this
- */
- public Mail setContent(String content) {
- this.content = content;
- return this;
- }
-
- /**
- * 设置是否是HTML
- *
- * @param isHtml 是否为HTML
- * @return this
- */
- public Mail setHtml(boolean isHtml) {
- this.isHtml = isHtml;
- return this;
- }
-
- /**
- * 设置正文
- *
- * @param content 正文内容
- * @param isHtml 是否为HTML
- * @return this
- */
- public Mail setContent(String content, boolean isHtml) {
- setContent(content);
- return setHtml(isHtml);
- }
-
- /**
- * 设置文件类型附件,文件可以是图片文件,此时自动设置cid(正文中引用图片),默认cid为文件名
- *
- * @param files 附件文件列表
- * @return this
- */
- public Mail setFiles(File... files) {
- if (ArrayUtil.isEmpty(files)) {
- return this;
- }
-
- final DataSource[] attachments = new DataSource[files.length];
- for (int i = 0; i < files.length; i++) {
- attachments[i] = new FileDataSource(files[i]);
- }
- return setAttachments(attachments);
- }
-
- /**
- * 增加附件或图片,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件
- *
- * @param attachments 附件列表
- * @return this
- * @since 4.0.9
- */
- public Mail setAttachments(DataSource... attachments) {
- if (ArrayUtil.isNotEmpty(attachments)) {
- final Charset charset = this.mailAccount.getCharset();
- MimeBodyPart bodyPart;
- String nameEncoded;
- try {
- for (DataSource attachment : attachments) {
- bodyPart = new MimeBodyPart();
- bodyPart.setDataHandler(new DataHandler(attachment));
- nameEncoded = attachment.getName();
- if (this.mailAccount.isEncodefilename()) {
- nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
- }
- // 普通附件文件名
- bodyPart.setFileName(nameEncoded);
- if (StrUtil.startWith(attachment.getContentType(), "image/")) {
- // 图片附件,用于正文中引用图片
- bodyPart.setContentID(nameEncoded);
- }
- this.multipart.addBodyPart(bodyPart);
- }
- } catch (MessagingException e) {
- throw new MailException(e);
- }
- }
- return this;
- }
-
- /**
- * 增加图片,图片的键对应到邮件模板中的占位字符串,图片类型默认为"image/jpeg"
- *
- * @param cid 图片与占位符,占位符格式为cid:${cid}
- * @param imageStream 图片文件
- * @return this
- * @since 4.6.3
- */
- public Mail addImage(String cid, InputStream imageStream) {
- return addImage(cid, imageStream, null);
- }
-
- /**
- * 增加图片,图片的键对应到邮件模板中的占位字符串
- *
- * @param cid 图片与占位符,占位符格式为cid:${cid}
- * @param imageStream 图片流,不关闭
- * @param contentType 图片类型,null赋值默认的"image/jpeg"
- * @return this
- * @since 4.6.3
- */
- public Mail addImage(String cid, InputStream imageStream, String contentType) {
- ByteArrayDataSource imgSource;
- try {
- imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
- } catch (IOException e) {
- throw new IORuntimeException(e);
- }
- imgSource.setName(cid);
- return setAttachments(imgSource);
- }
-
- /**
- * 增加图片,图片的键对应到邮件模板中的占位字符串
- *
- * @param cid 图片与占位符,占位符格式为cid:${cid}
- * @param imageFile 图片文件
- * @return this
- * @since 4.6.3
- */
- public Mail addImage(String cid, File imageFile) {
- InputStream in = null;
- try {
- in = FileUtil.getInputStream(imageFile);
- return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
- } finally {
- IoUtil.close(in);
- }
- }
-
- /**
- * 设置字符集编码
- *
- * @param charset 字符集编码
- * @return this
- * @see MailAccount#setCharset(Charset)
- */
- public Mail setCharset(Charset charset) {
- this.mailAccount.setCharset(charset);
- return this;
- }
-
- /**
- * 设置是否使用全局会话,默认为true
- *
- * @param isUseGlobalSession 是否使用全局会话,默认为true
- * @return this
- * @since 4.0.2
- */
- public Mail setUseGlobalSession(boolean isUseGlobalSession) {
- this.useGlobalSession = isUseGlobalSession;
- return this;
- }
-
- /**
- * 设置debug输出位置,可以自定义debug日志
- *
- * @param debugOutput debug输出位置
- * @return this
- * @since 5.5.6
- */
- public Mail setDebugOutput(PrintStream debugOutput) {
- this.debugOutput = debugOutput;
- return this;
- }
- // --------------------------------------------------------------- Getters and Setters end
-
- @Override
- public MimeMessage build() {
- try {
- return buildMsg();
- } catch (MessagingException e) {
- throw new MailException(e);
- }
- }
-
- /**
- * 发送
- *
- * @return message-id
- * @throws MailException 邮件发送异常
- */
- public String send() throws MailException {
- try {
- return doSend();
- } catch (MessagingException e) {
- if (e instanceof SendFailedException) {
- // 当地址无效时,显示更加详细的无效地址信息
- final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
- final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
- throw new MailException(msg, e);
- }
- throw new MailException(e);
- }
- }
-
- // --------------------------------------------------------------- Private method start
-
- /**
- * 执行发送
- *
- * @return message-id
- * @throws MessagingException 发送异常
- */
- private String doSend() throws MessagingException {
- final MimeMessage mimeMessage = buildMsg();
- Transport.send(mimeMessage);
- return mimeMessage.getMessageID();
- }
-
- /**
- * 构建消息
- *
- * @return {@link MimeMessage}消息
- * @throws MessagingException 消息异常
- */
- private MimeMessage buildMsg() throws MessagingException {
- final Charset charset = this.mailAccount.getCharset();
- final MimeMessage msg = new MimeMessage(getSession());
- // 发件人
- final String from = this.mailAccount.getFrom();
- if (StrUtil.isEmpty(from)) {
- // 用户未提供发送方,则从Session中自动获取
- msg.setFrom();
- } else {
- msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
- }
- // 标题
- msg.setSubject(this.title, (null == charset) ? null : charset.name());
- // 发送时间
- msg.setSentDate(new Date());
- // 内容和附件
- msg.setContent(buildContent(charset));
- // 收件人
- msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
- // 抄送人
- if (ArrayUtil.isNotEmpty(this.ccs)) {
- msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
- }
- // 密送人
- if (ArrayUtil.isNotEmpty(this.bccs)) {
- msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
- }
- // 回复地址(reply-to)
- if (ArrayUtil.isNotEmpty(this.reply)) {
- msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
- }
-
- return msg;
- }
-
- /**
- * 构建邮件信息主体
- *
- * @param charset 编码,{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}
- * @return 邮件信息主体
- * @throws MessagingException 消息异常
- */
- private Multipart buildContent(Charset charset) throws MessagingException {
- final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
- // 正文
- final MimeBodyPart body = new MimeBodyPart();
- body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
- this.multipart.addBodyPart(body);
-
- return this.multipart;
- }
-
- /**
- * 获取默认邮件会话
- * 如果为全局单例的会话,则全局只允许一个邮件帐号,否则每次发送邮件会新建一个新的会话
- *
- * @return 邮件会话 {@link Session}
- */
- private Session getSession() {
- final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);
-
- if (null != this.debugOutput) {
- session.setDebugOut(debugOutput);
- }
-
- return session;
- }
- // --------------------------------------------------------------- Private method end
-}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java
deleted file mode 100644
index 2a732a1a9..000000000
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java
+++ /dev/null
@@ -1,659 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.util.CharsetUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.setting.Setting;
-
-import java.io.Serial;
-import java.io.Serializable;
-import java.nio.charset.Charset;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-
-/**
- * 邮件账户对象
- *
- * @author Luxiaolei
- */
-public class MailAccount implements Serializable {
- @Serial
- private static final long serialVersionUID = -6937313421815719204L;
-
- private static final String MAIL_PROTOCOL = "mail.transport.protocol";
- private static final String SMTP_HOST = "mail.smtp.host";
- private static final String SMTP_PORT = "mail.smtp.port";
- private static final String SMTP_AUTH = "mail.smtp.auth";
- private static final String SMTP_TIMEOUT = "mail.smtp.timeout";
- private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
- private static final String SMTP_WRITE_TIMEOUT = "mail.smtp.writetimeout";
-
- // SSL
- private static final String STARTTLS_ENABLE = "mail.smtp.starttls.enable";
- private static final String SSL_ENABLE = "mail.smtp.ssl.enable";
- private static final String SSL_PROTOCOLS = "mail.smtp.ssl.protocols";
- private static final String SOCKET_FACTORY = "mail.smtp.socketFactory.class";
- private static final String SOCKET_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback";
- private static final String SOCKET_FACTORY_PORT = "smtp.socketFactory.port";
-
- // System Properties
- private static final String SPLIT_LONG_PARAMS = "mail.mime.splitlongparameters";
- //private static final String ENCODE_FILE_NAME = "mail.mime.encodefilename";
- //private static final String CHARSET = "mail.mime.charset";
-
- // 其他
- private static final String MAIL_DEBUG = "mail.debug";
-
- public static final String[] MAIL_SETTING_PATHS = new String[]{"config/mail.setting", "config/mailAccount.setting", "mail.setting"};
-
- /**
- * SMTP服务器域名
- */
- private String host;
- /**
- * SMTP服务端口
- */
- private Integer port;
- /**
- * 是否需要用户名密码验证
- */
- private Boolean auth;
- /**
- * 用户名
- */
- private String user;
- /**
- * 密码
- */
- private String pass;
- /**
- * 发送方,遵循RFC-822标准
- */
- private String from;
-
- /**
- * 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
- */
- private boolean debug;
- /**
- * 编码用于编码邮件正文和发送人、收件人等中文
- */
- private Charset charset = CharsetUtil.CHARSET_UTF_8;
- /**
- * 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
- */
- private boolean splitlongparameters = false;
- /**
- * 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
- */
- private boolean encodefilename = true;
-
- /**
- * 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
- */
- private boolean starttlsEnable = false;
- /**
- * 使用 SSL安全连接
- */
- private Boolean sslEnable;
-
- /**
- * SSL协议,多个协议用空格分隔
- */
- private String sslProtocols;
-
- /**
- * 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
- */
- private String socketFactoryClass = "javax.net.ssl.SSLSocketFactory";
- /**
- * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
- */
- private boolean socketFactoryFallback;
- /**
- * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
- */
- private int socketFactoryPort = 465;
-
- /**
- * SMTP超时时长,单位毫秒,缺省值不超时
- */
- private long timeout;
- /**
- * Socket连接超时值,单位毫秒,缺省值不超时
- */
- private long connectionTimeout;
- /**
- * Socket写出超时值,单位毫秒,缺省值不超时
- */
- private long writeTimeout;
-
- /**
- * 自定义的其他属性,此自定义属性会覆盖默认属性
- */
- private final Map customProperty = new HashMap<>();
-
- // -------------------------------------------------------------- Constructor start
-
- /**
- * 构造,所有参数需自行定义或保持默认值
- */
- public MailAccount() {
- }
-
- /**
- * 构造
- *
- * @param settingPath 配置文件路径
- */
- public MailAccount(String settingPath) {
- this(new Setting(settingPath));
- }
-
- /**
- * 构造
- *
- * @param setting 配置文件
- */
- public MailAccount(Setting setting) {
- setting.toBean(this);
- }
-
- // -------------------------------------------------------------- Constructor end
-
- /**
- * 获得SMTP服务器域名
- *
- * @return SMTP服务器域名
- */
- public String getHost() {
- return host;
- }
-
- /**
- * 设置SMTP服务器域名
- *
- * @param host SMTP服务器域名
- * @return this
- */
- public MailAccount setHost(String host) {
- this.host = host;
- return this;
- }
-
- /**
- * 获得SMTP服务端口
- *
- * @return SMTP服务端口
- */
- public Integer getPort() {
- return port;
- }
-
- /**
- * 设置SMTP服务端口
- *
- * @param port SMTP服务端口
- * @return this
- */
- public MailAccount setPort(Integer port) {
- this.port = port;
- return this;
- }
-
- /**
- * 是否需要用户名密码验证
- *
- * @return 是否需要用户名密码验证
- */
- public Boolean isAuth() {
- return auth;
- }
-
- /**
- * 设置是否需要用户名密码验证
- *
- * @param isAuth 是否需要用户名密码验证
- * @return this
- */
- public MailAccount setAuth(boolean isAuth) {
- this.auth = isAuth;
- return this;
- }
-
- /**
- * 获取用户名
- *
- * @return 用户名
- */
- public String getUser() {
- return user;
- }
-
- /**
- * 设置用户名
- *
- * @param user 用户名
- * @return this
- */
- public MailAccount setUser(String user) {
- this.user = user;
- return this;
- }
-
- /**
- * 获取密码
- *
- * @return 密码
- */
- public String getPass() {
- return pass;
- }
-
- /**
- * 设置密码
- *
- * @param pass 密码
- * @return this
- */
- public MailAccount setPass(String pass) {
- this.pass = pass;
- return this;
- }
-
- /**
- * 获取发送方,遵循RFC-822标准
- *
- * @return 发送方,遵循RFC-822标准
- */
- public String getFrom() {
- return from;
- }
-
- /**
- * 设置发送方,遵循RFC-822标准
- * 发件人可以是以下形式:
- *
- *
- * 1. user@xxx.xx
- * 2. name <user@xxx.xx>
- *
- *
- * @param from 发送方,遵循RFC-822标准
- * @return this
- */
- public MailAccount setFrom(String from) {
- this.from = from;
- return this;
- }
-
- /**
- * 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
- *
- * @return 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
- * @since 4.0.2
- */
- public boolean isDebug() {
- return debug;
- }
-
- /**
- * 设置是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
- *
- * @param debug 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
- * @return this
- * @since 4.0.2
- */
- public MailAccount setDebug(boolean debug) {
- this.debug = debug;
- return this;
- }
-
- /**
- * 获取字符集编码
- *
- * @return 编码,可能为{@code null}
- */
- public Charset getCharset() {
- return charset;
- }
-
- /**
- * 设置字符集编码,此选项不会修改全局配置,若修改全局配置,请设置此项为{@code null}并设置:
- *
- * System.setProperty("mail.mime.charset", charset);
- *
- *
- * @param charset 字符集编码,{@code null} 则表示使用全局设置的默认编码,全局编码为mail.mime.charset系统属性
- * @return this
- */
- public MailAccount setCharset(Charset charset) {
- this.charset = charset;
- return this;
- }
-
- /**
- * 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
- *
- * @return 对于超长参数是否切分为多份
- */
- public boolean isSplitlongparameters() {
- return splitlongparameters;
- }
-
- /**
- * 设置对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
- * 注意此项为全局设置,此项会调用
- *
- * System.setProperty("mail.mime.splitlongparameters", true)
- *
- *
- * @param splitlongparameters 对于超长参数是否切分为多份
- */
- public void setSplitlongparameters(boolean splitlongparameters) {
- this.splitlongparameters = splitlongparameters;
- }
-
- /**
- * 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
- *
- * @return 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
- * @since 5.7.16
- */
- public boolean isEncodefilename() {
-
- return encodefilename;
- }
-
- /**
- * 设置对于文件名是否使用{@link #charset}编码,此选项不会修改全局配置
- * 如果此选项设置为{@code false},则是否编码取决于两个系统属性:
- *
- * - mail.mime.encodefilename 是否编码附件文件名
- * - mail.mime.charset 编码文件名的编码
- *
- *
- * @param encodefilename 对于文件名是否使用{@link #charset}编码
- * @since 5.7.16
- */
- public void setEncodefilename(boolean encodefilename) {
- this.encodefilename = encodefilename;
- }
-
- /**
- * 是否使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
- *
- * @return 是否使用 STARTTLS安全连接
- */
- public boolean isStarttlsEnable() {
- return this.starttlsEnable;
- }
-
- /**
- * 设置是否使用STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
- *
- * @param startttlsEnable 是否使用STARTTLS安全连接
- * @return this
- */
- public MailAccount setStarttlsEnable(boolean startttlsEnable) {
- this.starttlsEnable = startttlsEnable;
- return this;
- }
-
- /**
- * 是否使用 SSL安全连接
- *
- * @return 是否使用 SSL安全连接
- */
- public Boolean isSslEnable() {
- return this.sslEnable;
- }
-
- /**
- * 设置是否使用SSL安全连接
- *
- * @param sslEnable 是否使用SSL安全连接
- * @return this
- */
- public MailAccount setSslEnable(Boolean sslEnable) {
- this.sslEnable = sslEnable;
- return this;
- }
-
- /**
- * 获取SSL协议,多个协议用空格分隔
- *
- * @return SSL协议,多个协议用空格分隔
- * @since 5.5.7
- */
- public String getSslProtocols() {
- return sslProtocols;
- }
-
- /**
- * 设置SSL协议,多个协议用空格分隔
- *
- * @param sslProtocols SSL协议,多个协议用空格分隔
- * @since 5.5.7
- */
- public void setSslProtocols(String sslProtocols) {
- this.sslProtocols = sslProtocols;
- }
-
- /**
- * 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
- *
- * @return 指定实现javax.net.SocketFactory接口的类的名称, 这个类将被用于创建SMTP的套接字
- */
- public String getSocketFactoryClass() {
- return socketFactoryClass;
- }
-
- /**
- * 设置指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
- *
- * @param socketFactoryClass 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
- * @return this
- */
- public MailAccount setSocketFactoryClass(String socketFactoryClass) {
- this.socketFactoryClass = socketFactoryClass;
- return this;
- }
-
- /**
- * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
- *
- * @return 如果设置为true, 未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
- */
- public boolean isSocketFactoryFallback() {
- return socketFactoryFallback;
- }
-
- /**
- * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
- *
- * @param socketFactoryFallback 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
- * @return this
- */
- public MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) {
- this.socketFactoryFallback = socketFactoryFallback;
- return this;
- }
-
- /**
- * 获取指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
- *
- * @return 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
- */
- public int getSocketFactoryPort() {
- return socketFactoryPort;
- }
-
- /**
- * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
- *
- * @param socketFactoryPort 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
- * @return this
- */
- public MailAccount setSocketFactoryPort(int socketFactoryPort) {
- this.socketFactoryPort = socketFactoryPort;
- return this;
- }
-
- /**
- * 设置SMTP超时时长,单位毫秒,缺省值不超时
- *
- * @param timeout SMTP超时时长,单位毫秒,缺省值不超时
- * @return this
- * @since 4.1.17
- */
- public MailAccount setTimeout(long timeout) {
- this.timeout = timeout;
- return this;
- }
-
- /**
- * 设置Socket连接超时值,单位毫秒,缺省值不超时
- *
- * @param connectionTimeout Socket连接超时值,单位毫秒,缺省值不超时
- * @return this
- * @since 4.1.17
- */
- public MailAccount setConnectionTimeout(long connectionTimeout) {
- this.connectionTimeout = connectionTimeout;
- return this;
- }
-
- /**
- * 设置Socket写出超时值,单位毫秒,缺省值不超时
- *
- * @param writeTimeout Socket写出超时值,单位毫秒,缺省值不超时
- * @return this
- * @since 5.8.3
- */
- public MailAccount setWriteTimeout(long writeTimeout) {
- this.writeTimeout = writeTimeout;
- return this;
- }
-
- /**
- * 获取自定义属性列表
- *
- * @return 自定义参数列表
- * @since 5.6.4
- */
- public Map getCustomProperty() {
- return customProperty;
- }
-
- /**
- * 设置自定义属性,如mail.smtp.ssl.socketFactory
- *
- * @param key 属性名,空白被忽略
- * @param value 属性值, null被忽略
- * @return this
- * @since 5.6.4
- */
- public MailAccount setCustomProperty(String key, Object value) {
- if (StrUtil.isNotBlank(key) && ObjectUtil.isNotNull(value)) {
- this.customProperty.put(key, value);
- }
- return this;
- }
-
- /**
- * 获得SMTP相关信息
- *
- * @return {@link Properties}
- */
- public Properties getSmtpProps() {
- //全局系统参数
- System.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters));
-
- final Properties p = new Properties();
- p.put(MAIL_PROTOCOL, "smtp");
- p.put(SMTP_HOST, this.host);
- p.put(SMTP_PORT, String.valueOf(this.port));
- p.put(SMTP_AUTH, String.valueOf(this.auth));
- if (this.timeout > 0) {
- p.put(SMTP_TIMEOUT, String.valueOf(this.timeout));
- }
- if (this.connectionTimeout > 0) {
- p.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout));
- }
- // issue#2355
- if (this.writeTimeout > 0) {
- p.put(SMTP_WRITE_TIMEOUT, String.valueOf(this.writeTimeout));
- }
-
- p.put(MAIL_DEBUG, String.valueOf(this.debug));
-
- if (this.starttlsEnable) {
- //STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
- p.put(STARTTLS_ENABLE, "true");
-
- if (null == this.sslEnable) {
- //为了兼容旧版本,当用户没有此项配置时,按照starttlsEnable开启状态时对待
- this.sslEnable = true;
- }
- }
-
- // SSL
- if (null != this.sslEnable && this.sslEnable) {
- p.put(SSL_ENABLE, "true");
- p.put(SOCKET_FACTORY, socketFactoryClass);
- p.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback));
- p.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort));
- // issue#IZN95@Gitee,在Linux下需自定义SSL协议版本
- if (StrUtil.isNotBlank(this.sslProtocols)) {
- p.put(SSL_PROTOCOLS, this.sslProtocols);
- }
- }
-
- // 补充自定义属性,允许自定属性覆盖已经设置的值
- p.putAll(this.customProperty);
-
- return p;
- }
-
- /**
- * 如果某些值为null,使用默认值
- *
- * @return this
- */
- public MailAccount defaultIfEmpty() {
- // 去掉发件人的姓名部分
- final String fromAddress = InternalMailUtil.parseFirstAddress(this.from, this.charset).getAddress();
-
- if (StrUtil.isBlank(this.host)) {
- // 如果SMTP地址为空,默认使用smtp.<发件人邮箱后缀>
- this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));
- }
- if (StrUtil.isBlank(user)) {
- // 如果用户名为空,默认为发件人(issue#I4FYVY@Gitee)
- //this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
- this.user = fromAddress;
- }
- if (null == this.auth) {
- // 如果密码非空白,则使用认证模式
- this.auth = (false == StrUtil.isBlank(this.pass));
- }
- if (null == this.port) {
- // 端口在SSL状态下默认与socketFactoryPort一致,非SSL状态下默认为25
- this.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25;
- }
- if (null == this.charset) {
- // 默认UTF-8编码
- this.charset = CharsetUtil.CHARSET_UTF_8;
- }
-
- return this;
- }
-
- @Override
- public String toString() {
- return "MailAccount [host=" + host + ", port=" + port + ", auth=" + auth + ", user=" + user + ", pass=" + (StrUtil.isEmpty(this.pass) ? "" : "******") + ", from=" + from + ", startttlsEnable="
- + starttlsEnable + ", socketFactoryClass=" + socketFactoryClass + ", socketFactoryFallback=" + socketFactoryFallback + ", socketFactoryPort=" + socketFactoryPort + "]";
- }
-}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java
deleted file mode 100644
index cc199d455..000000000
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.exceptions.ExceptionUtil;
-import cn.hutool.core.util.StrUtil;
-
-import java.io.Serial;
-
-/**
- * 邮件异常
- *
- * @author xiaoleilu
- */
-public class MailException extends RuntimeException {
- @Serial
- private static final long serialVersionUID = 8247610319171014183L;
-
- public MailException(Throwable e) {
- super(ExceptionUtil.getMessage(e), e);
- }
-
- public MailException(String message) {
- super(message);
- }
-
- public MailException(String messageTemplate, Object... params) {
- super(StrUtil.format(messageTemplate, params));
- }
-
- public MailException(String message, Throwable throwable) {
- super(message, throwable);
- }
-
- public MailException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
- super(message, throwable, enableSuppression, writableStackTrace);
- }
-
- public MailException(Throwable throwable, String messageTemplate, Object... params) {
- super(StrUtil.format(messageTemplate, params), throwable);
- }
-}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java
index 040cc572a..a28701fbc 100644
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java
+++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java
@@ -5,6 +5,9 @@
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.mail.JakartaMail;
+import cn.hutool.extra.mail.JakartaUserPassAuthenticator;
+import cn.hutool.extra.mail.MailAccount;
import jakarta.mail.Authenticator;
import jakarta.mail.Session;
import lombok.AccessLevel;
@@ -17,7 +20,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
-
+import java.util.Map.Entry;
/**
* 邮件工具类
@@ -385,7 +388,7 @@ public static String send(MailAccount mailAccount, Collection tos, Colle
public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
Authenticator authenticator = null;
if (mailAccount.isAuth()) {
- authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
+ authenticator = new JakartaUserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
}
return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
@@ -412,7 +415,7 @@ public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
*/
private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection tos, Collection ccs, Collection bccs, String subject, String content,
Map imageMap, boolean isHtml, File... files) {
- final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
+ final JakartaMail mail = JakartaMail.create(mailAccount).setUseGlobalSession(useGlobalSession);
// 可选抄送人
if (CollUtil.isNotEmpty(ccs)) {
@@ -431,7 +434,7 @@ private static String send(MailAccount mailAccount, boolean useGlobalSession, Co
// 图片
if (MapUtil.isNotEmpty(imageMap)) {
- for (Map.Entry entry : imageMap.entrySet()) {
+ for (Entry entry : imageMap.entrySet()) {
mail.addImage(entry.getKey(), entry.getValue());
// 关闭流
IoUtil.close(entry.getValue());
@@ -463,5 +466,4 @@ private static List splitAddress(String addresses) {
return result;
}
// ------------------------------------------------------------------------------------------------------------------------ Private method end
-
}
diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java
deleted file mode 100644
index fbbe5e371..000000000
--- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import jakarta.mail.Authenticator;
-import jakarta.mail.PasswordAuthentication;
-
-/**
- * 用户名密码验证器
- *
- * @author looly
- * @since 3.1.2
- */
-public class UserPassAuthenticator extends Authenticator {
-
- private final String user;
- private final String pass;
-
- /**
- * 构造
- *
- * @param user 用户名
- * @param pass 密码
- */
- public UserPassAuthenticator(String user, String pass) {
- this.user = user;
- this.pass = pass;
- }
-
- @Override
- protected PasswordAuthentication getPasswordAuthentication() {
- return new PasswordAuthentication(this.user, this.pass);
- }
-
-}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java
index f8c5cd009..2879b9d1c 100644
--- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java
@@ -30,4 +30,11 @@
*/
String[] value() default "dept_id";
+ /**
+ * 权限标识符 用于通过菜单权限标识符来获取数据权限
+ * 拥有此标识符的角色 将不会拼接此角色的数据过滤sql
+ *
+ * @return 权限标识符
+ */
+ String permission() default "";
}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java
index 6fd3c3e07..f5f22d599 100644
--- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java
@@ -20,4 +20,11 @@
*/
DataColumn[] value();
+ /**
+ * 权限拼接标识符(用于指定连接语句的sql符号)
+ * 如不填 默认 select 用 OR 其他语句用 AND
+ * 内容 OR 或者 AND
+ */
+ String joinStr() default "";
+
}
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java
index 455cecb2e..981bd421b 100644
--- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java
@@ -13,9 +13,9 @@
* 内置数据:
* - {@code user}: 当前登录用户信息,参考 {@link LoginUser}
* 内置服务:
- * - {@code sdss}: 系统数据权限服务,参考 {@link ISysDataScopeService}
+ * - {@code sdss}: 系统数据权限服务,参考 ISysDataScopeService
* 如需扩展数据,可以通过 {@link DataPermissionHelper} 进行操作
- * 如需扩展服务,可以通过 {@link ISysDataScopeService} 自行编写
+ * 如需扩展服务,可以通过 ISysDataScopeService 自行编写
*
*
* @author Lion Li
@@ -32,29 +32,21 @@ public enum DataScopeType {
/**
* 自定数据权限
- * 使用 SpEL 表达式:`#{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} )`
- * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", " 1 = 0 "),
/**
* 部门数据权限
- * 使用 SpEL 表达式:`#{#deptName} = #{#user.deptId}`
- * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "),
/**
* 部门及以下数据权限
- * 使用 SpEL 表达式:`#{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )}`
- * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "),
/**
* 仅本人数据权限
- * 使用 SpEL 表达式:`#{#userName} = #{#user.userId}`
- * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
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 99e6b3888..7d44d2648 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
@@ -48,6 +48,10 @@ public void insertFill(MetaObject metaObject) {
? baseEntity.getCreateDept() : loginUser.getDeptId());
}
}
+ } else {
+ Date date = new Date();
+ this.strictInsertFill(metaObject, "createTime", Date.class, date);
+ this.strictInsertFill(metaObject, "updateTime", Date.class, date);
}
} catch (Exception e) {
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
@@ -72,6 +76,8 @@ public void updateFill(MetaObject metaObject) {
if (ObjectUtil.isNotNull(userId)) {
baseEntity.setUpdateBy(userId);
}
+ } else {
+ this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
} 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/PlusDataPermissionHandler.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
index 74279bde0..5ac74c321 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
@@ -99,7 +99,7 @@ public Expression getSqlSegment(Expression where, String mappedStatementId, bool
return where;
}
// 构造数据过滤条件的 SQL 片段
- String dataFilterSql = buildDataFilter(dataPermission.value(), isSelect);
+ String dataFilterSql = buildDataFilter(dataPermission, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
@@ -120,14 +120,17 @@ public Expression getSqlSegment(Expression where, String mappedStatementId, bool
/**
* 构建数据过滤条件的 SQL 语句
*
- * @param dataColumns 数据权限注解中的列信息
- * @param isSelect 标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
+ * @param dataPermission 数据权限注解
+ * @param isSelect 标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
* @return 构建的数据过滤条件的 SQL 语句
* @throws ServiceException 如果角色的数据范围异常或者 key 与 value 的长度不匹配,则抛出 ServiceException 异常
*/
- private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
+ private String buildDataFilter(DataPermission dataPermission, boolean isSelect) {
// 更新或删除需满足所有条件
String joinStr = isSelect ? " OR " : " AND ";
+ if (StringUtils.isNotBlank(dataPermission.joinStr())) {
+ joinStr = " " + dataPermission.joinStr() + " ";
+ }
LoginUser user = DataPermissionHelper.getVariable("user");
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(beanResolver);
@@ -145,7 +148,7 @@ private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
return "";
}
boolean isSuccess = false;
- for (DataColumn dataColumn : dataColumns) {
+ for (DataColumn dataColumn : dataPermission.value()) {
if (dataColumn.key().length != dataColumn.value().length) {
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
}
@@ -155,6 +158,13 @@ private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
)) {
continue;
}
+ // 包含权限标识符 这直接跳过
+ if (StringUtils.isNotBlank(dataColumn.permission()) &&
+ CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
+ ) {
+ isSuccess = true;
+ continue;
+ }
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java
index 2afe9ee47..932f17388 100644
--- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java
+++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java
@@ -2,14 +2,17 @@
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
+import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.HashMap;
import java.util.Map;
+import java.util.Stack;
import java.util.function.Supplier;
/**
@@ -24,6 +27,8 @@ public class DataPermissionHelper {
private static final String DATA_PERMISSION_KEY = "data:permission";
+ private static final ThreadLocal> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
+
/**
* 从上下文中获取指定键的变量值,并将其转换为指定的类型
*
@@ -66,23 +71,54 @@ public static Map getContext() {
throw new NullPointerException("data permission context type exception");
}
+ private static IgnoreStrategy getIgnoreStrategy() {
+ Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
+ if (ignoreStrategyLocal instanceof ThreadLocal> IGNORE_STRATEGY_LOCAL) {
+ if (IGNORE_STRATEGY_LOCAL.get() instanceof IgnoreStrategy ignoreStrategy) {
+ return ignoreStrategy;
+ }
+ }
+ return null;
+ }
+
/**
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
- InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
+ IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
+ if (ObjectUtil.isNull(ignoreStrategy)) {
+ InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
+ } else {
+ ignoreStrategy.setDataPermission(true);
+ }
+ Stack reentrantStack = REENTRANT_IGNORE.get();
+ reentrantStack.push(reentrantStack.size() + 1);
}
/**
* 关闭忽略数据权限
*/
public static void disableIgnore() {
- InterceptorIgnoreHelper.clearIgnoreStrategy();
+ IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
+ if (ObjectUtil.isNotNull(ignoreStrategy)) {
+ boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
+ && !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
+ && !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
+ && !Boolean.TRUE.equals(ignoreStrategy.getTenantLine())
+ && CollectionUtil.isEmpty(ignoreStrategy.getOthers());
+ Stack reentrantStack = REENTRANT_IGNORE.get();
+ boolean empty = reentrantStack.isEmpty() || reentrantStack.pop() == 1;
+ if (noOtherIgnoreStrategy && empty) {
+ InterceptorIgnoreHelper.clearIgnoreStrategy();
+ } else if (empty) {
+ ignoreStrategy.setDataPermission(false);
+ }
+
+ }
}
/**
* 在忽略数据权限中执行
- * 禁止在忽略数据权限中执行忽略数据权限
*
* @param handle 处理执行方法
*/
@@ -97,7 +133,6 @@ public static void ignore(Runnable handle) {
/**
* 在忽略数据权限中执行
- * 禁止在忽略数据权限中执行忽略数据权限
*
* @param handle 处理执行方法
*/
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 02735b073..1f4904a3e 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
@@ -80,11 +80,11 @@ public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
- if (StringUtils.isNotBlank(key)) {
+ // 判断 key 不为空 和 不是表达式
+ if (StringUtils.isNotBlank(key) && StringUtils.containsAny(key, "#")) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method targetMethod = signature.getMethod();
Object[] args = point.getArgs();
- //noinspection DataFlowIssue
MethodBasedEvaluationContext context =
new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java
index ee1d405f2..793e21f5c 100644
--- a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java
@@ -15,15 +15,17 @@ public class CaffeineCacheDecorator implements Cache {
private static final com.github.benmanes.caffeine.cache.Cache