# Conflicts:
#	ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
This commit is contained in:
insist 2024-12-21 16:39:38 +08:00
commit 26ff6386d9
151 changed files with 2212 additions and 952 deletions

View File

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="演示机"> <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="ruoyi/ruoyi-server:5.2.3" /> <option name="imageTag" value="ruoyi/ruoyi-server:5.2.3" />

View File

@ -6,22 +6,25 @@
[![码云Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![码云Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![GitHub](https://img.shields.io/github/stars/dromara/RuoYi-Vue-Plus.svg?style=social&label=Stars)](https://github.com/dromara/RuoYi-Vue-Plus) [![GitHub](https://img.shields.io/github/stars/dromara/RuoYi-Vue-Plus.svg?style=social&label=Stars)](https://github.com/dromara/RuoYi-Vue-Plus)
[![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE) [![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) [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
<br> <br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.3-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.3-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2-blue.svg)]() [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.3-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]() [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]() [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
> RuoYi-Vue-Plus 是重写 RuoYi-Vue 针对 `分布式集群与多租户` 场景全方位升级(不兼容原框架) > Dromara RuoYi-Vue-Plus 是重写 RuoYi-Vue 针对 `分布式集群与多租户` 场景全方位升级(不兼容原框架)
> 项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可<br> > 项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可<br>
活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源 活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源
> 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system) > 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
> 前端项目地址: [plus-ui](https://gitee.com/JavaLionLi/plus-ui) > 官方前端项目地址: [plus-ui](https://gitee.com/JavaLionLi/plus-ui)<br>
> 成员前端项目地址: 基于vben [ruoyi-plus-vben](https://gitee.com/dapppp/ruoyi-plus-vben)<br>
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
> 文档地址: [plus-doc](https://plus-doc.dromara.org) > 文档地址: [plus-doc](https://plus-doc.dromara.org)
@ -31,6 +34,7 @@ MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey <br>
CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br> CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br> 数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br>
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br> 引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
<font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
[如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group) [如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
# 本框架与RuoYi的功能差异 # 本框架与RuoYi的功能差异

26
pom.xml
View File

@ -10,33 +10,33 @@
<name>RuoYi-Vue-Plus</name> <name>RuoYi-Vue-Plus</name>
<url>https://gitee.com/dromara/RuoYi-Vue-Plus</url> <url>https://gitee.com/dromara/RuoYi-Vue-Plus</url>
<description>RuoYi-Vue-Plus多租户管理系统</description> <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
<properties> <properties>
<revision>5.2.3</revision> <revision>5.2.3</revision>
<spring-boot.version>3.2.11</spring-boot.version> <spring-boot.version>3.3.5</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version> <java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version> <mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.6.0</springdoc.version> <springdoc.version>2.7.0</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version> <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<easyexcel.version>4.0.3</easyexcel.version> <easyexcel.version>4.0.3</easyexcel.version>
<velocity.version>2.3</velocity.version> <velocity.version>2.3</velocity.version>
<satoken.version>1.39.0</satoken.version> <satoken.version>1.39.0</satoken.version>
<mybatis-plus.version>3.5.8</mybatis-plus.version> <mybatis-plus.version>3.5.9</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version> <p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.31</hutool.version> <hutool.version>5.8.31</hutool.version>
<spring-boot-admin.version>3.2.3</spring-boot-admin.version> <spring-boot-admin.version>3.3.4</spring-boot-admin.version>
<redisson.version>3.37.0</redisson.version> <redisson.version>3.39.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version> <lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version> <dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.1.2</snailjob.version> <snailjob.version>1.2.0</snailjob.version>
<mapstruct-plus.version>1.4.5</mapstruct-plus.version> <mapstruct-plus.version>1.4.5</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version> <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.34</lombok.version> <lombok.version>1.18.34</lombok.version>
<bouncycastle.version>1.76</bouncycastle.version> <bouncycastle.version>1.76</bouncycastle.version>
<justauth.version>1.16.6</justauth.version> <justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 --> <!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
@ -55,7 +55,7 @@
<!-- 插件版本 --> <!-- 插件版本 -->
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version> <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
<maven-war-plugin.version>3.2.2</maven-war-plugin.version> <maven-war-plugin.version>3.2.2</maven-war-plugin.version>
<maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison> <maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version> <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
</properties> </properties>
@ -216,6 +216,12 @@
<version>${mybatis-plus.version}</version> <version>${mybatis-plus.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-annotation</artifactId> <artifactId>mybatis-plus-annotation</artifactId>
@ -369,7 +375,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.verison}</version> <version>${maven-compiler-plugin.version}</version>
<configuration> <configuration>
<source>${java.version}</source> <source>${java.version}</source>
<target>${java.version}</target> <target>${java.version}</target>

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-debian:17.0.11-cds FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
#FROM bellsoft/liberica-openjdk-debian:21.0.3-cds #FROM bellsoft/liberica-openjdk-debian:21.0.5-cds
#FROM findepi/graalvm:java17-native #FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li" LABEL maintainer="Lion Li"
@ -17,6 +17,8 @@ EXPOSE ${SERVER_PORT}
ADD ./target/ruoyi-admin.jar ./app.jar ADD ./target/ruoyi-admin.jar ./app.jar
SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \ ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
# 应用名称 如果想区分集群节点监控 改成不同的名称即可 # 应用名称 如果想区分集群节点监控 改成不同的名称即可
#-Dskywalking.agent.service_name=ruoyi-server \ #-Dskywalking.agent.service_name=ruoyi-server \

View File

@ -12,7 +12,7 @@ import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils; import me.zhyd.oauth.utils.AuthStateUtils;
import org.dromara.common.core.constant.UserConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.LoginBody; import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.domain.model.RegisterBody; import org.dromara.common.core.domain.model.RegisterBody;
@ -92,7 +92,7 @@ public class AuthController {
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) { if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType); log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
return R.fail(MessageUtils.message("auth.grant.type.error")); return R.fail(MessageUtils.message("auth.grant.type.error"));
} else if (!UserConstants.NORMAL.equals(client.getStatus())) { } else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
return R.fail(MessageUtils.message("auth.grant.type.blocked")); return R.fail(MessageUtils.message("auth.grant.type.blocked"));
} }
// 校验租户 // 校验租户
@ -226,7 +226,7 @@ public class AuthController {
} }
// 根据域名进行筛选 // 根据域名进行筛选
List<TenantListVo> list = StreamUtils.filter(voList, vo -> List<TenantListVo> list = StreamUtils.filter(voList, vo ->
StringUtils.equals(vo.getDomain(), host)); StringUtils.equalsIgnoreCase(vo.getDomain(), host));
result.setVoList(CollUtil.isNotEmpty(list) ? list : voList); result.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
return R.ok(result); return R.ok(result);
} }

View File

@ -12,11 +12,12 @@ import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.CacheConstants; import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.constant.TenantConstants; import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.domain.dto.PostDTO;
import org.dromara.common.core.domain.dto.RoleDTO; import org.dromara.common.core.domain.dto.RoleDTO;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.enums.TenantStatus;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.*; import org.dromara.common.core.utils.*;
@ -60,6 +61,7 @@ public class SysLoginService {
private final ISysSocialService sysSocialService; private final ISysSocialService sysSocialService;
private final ISysRoleService roleService; private final ISysRoleService roleService;
private final ISysDeptService deptService; private final ISysDeptService deptService;
private final ISysPostService postService;
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
@ -148,21 +150,24 @@ public class SysLoginService {
*/ */
public LoginUser buildLoginUser(SysUserVo user) { public LoginUser buildLoginUser(SysUserVo user) {
LoginUser loginUser = new LoginUser(); LoginUser loginUser = new LoginUser();
Long userId = user.getUserId();
loginUser.setTenantId(user.getTenantId()); loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(user.getUserId()); loginUser.setUserId(userId);
loginUser.setDeptId(user.getDeptId()); loginUser.setDeptId(user.getDeptId());
loginUser.setUsername(user.getUserName()); loginUser.setUsername(user.getUserName());
loginUser.setNickname(user.getNickName()); loginUser.setNickname(user.getNickName());
loginUser.setUserType(user.getUserType()); loginUser.setUserType(user.getUserType());
loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId())); loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId())); loginUser.setRolePermission(permissionService.getRolePermission(userId));
if (ObjectUtil.isNotNull(user.getDeptId())) { if (ObjectUtil.isNotNull(user.getDeptId())) {
Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById); Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY)); loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY)); loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
} }
List<SysRoleVo> roles = roleService.selectRolesByUserId(user.getUserId()); List<SysRoleVo> roles = roleService.selectRolesByUserId(userId);
List<SysPostVo> posts = postService.selectPostsByUserId(userId);
loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class)); loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
return loginUser; return loginUser;
} }
@ -223,17 +228,17 @@ public class SysLoginService {
if (!TenantHelper.isEnable()) { if (!TenantHelper.isEnable()) {
return; return;
} }
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
return;
}
if (StringUtils.isBlank(tenantId)) { if (StringUtils.isBlank(tenantId)) {
throw new TenantException("tenant.number.not.blank"); throw new TenantException("tenant.number.not.blank");
} }
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
return;
}
SysTenantVo tenant = tenantService.queryByTenantId(tenantId); SysTenantVo tenant = tenantService.queryByTenantId(tenantId);
if (ObjectUtil.isNull(tenant)) { if (ObjectUtil.isNull(tenant)) {
log.info("登录租户:{} 不存在.", tenantId); log.info("登录租户:{} 不存在.", tenantId);
throw new TenantException("tenant.not.exists"); throw new TenantException("tenant.not.exists");
} else if (TenantStatus.DISABLE.getCode().equals(tenant.getStatus())) { } else if (SystemConstants.DISABLE.equals(tenant.getStatus())) {
log.info("登录租户:{} 已被停用.", tenantId); log.info("登录租户:{} 已被停用.", tenantId);
throw new TenantException("tenant.blocked"); throw new TenantException("tenant.blocked");
} else if (ObjectUtil.isNotNull(tenant.getExpireTime()) } else if (ObjectUtil.isNotNull(tenant.getExpireTime())

View File

@ -84,11 +84,11 @@ public class SysRegisterService {
String captcha = RedisUtils.getCacheObject(verifyKey); String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey); RedisUtils.deleteObject(verifyKey);
if (captcha == null) { if (captcha == null) {
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.expire")); recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!code.equalsIgnoreCase(captcha)) { if (!code.equalsIgnoreCase(captcha)) {
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error")); recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException(); throw new CaptchaException();
} }
} }

View File

@ -8,10 +8,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.EmailLoginBody; import org.dromara.common.core.domain.model.EmailLoginBody;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.enums.UserStatus;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils; import org.dromara.common.core.utils.MessageUtils;
@ -92,7 +92,7 @@ public class EmailAuthStrategy implements IAuthStrategy {
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", email); log.info("登录用户:{} 不存在.", email);
throw new UserException("user.not.exists", email); throw new UserException("user.not.exists", email);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", email); log.info("登录用户:{} 已被停用.", email);
throw new UserException("user.blocked", email); throw new UserException("user.blocked", email);
} }

View File

@ -9,10 +9,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.PasswordLoginBody; import org.dromara.common.core.domain.model.PasswordLoginBody;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.enums.UserStatus;
import org.dromara.common.core.exception.user.CaptchaException; import org.dromara.common.core.exception.user.CaptchaException;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
@ -113,7 +113,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username); log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username); throw new UserException("user.not.exists", username);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username); log.info("登录用户:{} 已被停用.", username);
throw new UserException("user.blocked", username); throw new UserException("user.blocked", username);
} }

View File

@ -8,10 +8,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.SmsLoginBody; import org.dromara.common.core.domain.model.SmsLoginBody;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.enums.UserStatus;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils; import org.dromara.common.core.utils.MessageUtils;
@ -92,7 +92,7 @@ public class SmsAuthStrategy implements IAuthStrategy {
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber); log.info("登录用户:{} 不存在.", phonenumber);
throw new UserException("user.not.exists", phonenumber); throw new UserException("user.not.exists", phonenumber);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phonenumber); log.info("登录用户:{} 已被停用.", phonenumber);
throw new UserException("user.blocked", phonenumber); throw new UserException("user.blocked", phonenumber);
} }

View File

@ -11,9 +11,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse; import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.SocialLoginBody; import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.enums.UserStatus;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StreamUtils;
@ -121,7 +121,7 @@ public class SocialAuthStrategy implements IAuthStrategy {
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", ""); log.info("登录用户:{} 不存在.", "");
throw new UserException("user.not.exists", ""); throw new UserException("user.not.exists", "");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", ""); log.info("登录用户:{} 已被停用.", "");
throw new UserException("user.blocked", ""); throw new UserException("user.blocked", "");
} }

View File

@ -5,13 +5,20 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.request.AuthWechatMiniProgramRequest;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.XcxLoginBody; import org.dromara.common.core.domain.model.XcxLoginBody;
import org.dromara.common.core.domain.model.XcxLoginUser; import org.dromara.common.core.domain.model.XcxLoginUser;
import org.dromara.common.core.enums.UserStatus; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ValidatorUtils; import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.SysClient;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.web.domain.vo.LoginVo; import org.dromara.web.domain.vo.LoginVo;
@ -40,12 +47,24 @@ public class XcxAuthStrategy implements IAuthStrategy {
// 多个小程序识别使用 // 多个小程序识别使用
String appid = loginBody.getAppid(); String appid = loginBody.getAppid();
// todo 以下自行实现
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key openid // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key openid
String openid = ""; AuthRequest authRequest = new AuthWechatMiniProgramRequest(AuthConfig.builder()
.clientId(appid).clientSecret("自行填写密钥 可根据不同appid填入不同密钥")
.ignoreCheckRedirectUri(true).ignoreCheckState(true).build());
AuthCallback authCallback = new AuthCallback();
authCallback.setCode(xcxCode);
AuthResponse<AuthUser> resp = authRequest.login(authCallback);
String openid, unionId;
if (resp.ok()) {
AuthToken token = resp.getData().getToken();
openid = token.getOpenId();
// 微信小程序只有关联到微信开放平台下之后才能获取到 unionId因此unionId不一定能返回
unionId = token.getUnionId();
} else {
throw new ServiceException(resp.getMsg());
}
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可 // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUserVo user = loadUserByOpenid(openid); SysUserVo user = loadUserByOpenid(openid);
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了 // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
XcxLoginUser loginUser = new XcxLoginUser(); XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setTenantId(user.getTenantId()); loginUser.setTenantId(user.getTenantId());
@ -82,7 +101,7 @@ public class XcxAuthStrategy implements IAuthStrategy {
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", openid); log.info("登录用户:{} 不存在.", openid);
// todo 用户不存在 业务逻辑自行实现 // todo 用户不存在 业务逻辑自行实现
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", openid); log.info("登录用户:{} 已被停用.", openid);
// todo 用户已被停用 业务逻辑自行实现 // todo 用户已被停用 业务逻辑自行实现
} }

View File

@ -2,7 +2,7 @@
spring.boot.admin.client: spring.boot.admin.client:
# 增加客户端开关 # 增加客户端开关
enabled: true enabled: true
url: http://localhost:9090/admin url: http://localhost:9090
instance: instance:
service-host-type: IP service-host-type: IP
metadata: metadata:
@ -200,7 +200,7 @@ justauth:
redirect-uri: ${justauth.address}/social-callback?source=maxkey redirect-uri: ${justauth.address}/social-callback?source=maxkey
topiam: topiam:
# topiam 服务器地址 # topiam 服务器地址
server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol server-url: http://127.0.0.1:1898/api/v1/authorize/y0q************spq***********8ol
client-id: 449c4*********937************759 client-id: 449c4*********937************759
client-secret: ac7***********1e0************28d client-secret: ac7***********1e0************28d
redirect-uri: ${justauth.address}/social-callback?source=topiam redirect-uri: ${justauth.address}/social-callback?source=topiam

View File

@ -5,7 +5,7 @@ spring.servlet.multipart.location: /ruoyi/server/temp
spring.boot.admin.client: spring.boot.admin.client:
# 增加客户端开关 # 增加客户端开关
enabled: true enabled: true
url: http://localhost:9090/admin url: http://localhost:9090
instance: instance:
service-host-type: IP service-host-type: IP
metadata: metadata:

View File

@ -47,6 +47,7 @@ logging:
org.dromara: @logging.level@ org.dromara: @logging.level@
org.springframework: warn org.springframework: warn
org.mybatis.spring.mapper: error org.mybatis.spring.mapper: error
org.apache.fury: warn
config: classpath:logback-plus.xml config: classpath:logback-plus.xml
# 用户配置 # 用户配置
@ -110,15 +111,12 @@ sa-token:
security: security:
# 排除路径 # 排除路径
excludes: excludes:
# 静态资源
- /*.html - /*.html
- /**/*.html - /**/*.html
- /**/*.css - /**/*.css
- /**/*.js - /**/*.js
# 公共路径
- /favicon.ico - /favicon.ico
- /error - /error
# swagger 文档配置
- /*/api-docs - /*/api-docs
- /*/api-docs/** - /*/api-docs/**
@ -141,6 +139,8 @@ tenant:
# MyBatisPlus配置 # MyBatisPlus配置
# https://baomidou.com/config/ # https://baomidou.com/config/
mybatis-plus: mybatis-plus:
# 自定义配置 是否全局开启逻辑删除 关闭后 所有逻辑删除功能将失效
enableLogicDelete: true
# 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper # 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
mapperPackage: org.dromara.**.mapper mapperPackage: org.dromara.**.mapper
# 对应的 XML 文件位置 # 对应的 XML 文件位置
@ -217,6 +217,8 @@ springdoc:
packages-to-scan: org.dromara.system packages-to-scan: org.dromara.system
- group: 4.代码生成模块 - group: 4.代码生成模块
packages-to-scan: org.dromara.generator packages-to-scan: org.dromara.generator
- group: 5.工作流模块
packages-to-scan: org.dromara.workflow
# 防止XSS攻击 # 防止XSS攻击
xss: xss:

View File

@ -2,7 +2,7 @@
<configuration> <configuration>
<property name="log.path" value="./logs"/> <property name="log.path" value="./logs"/>
<property name="console.log.pattern" <property name="console.log.pattern"
value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/> value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/> <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 控制台输出 --> <!-- 控制台输出 -->

View File

@ -60,6 +60,16 @@ public interface CacheNames {
*/ */
String SYS_OSS = "sys_oss#30d"; String SYS_OSS = "sys_oss#30d";
/**
* 角色自定义权限
*/
String SYS_ROLE_CUSTOM = "sys_role_custom#30d";
/**
* 部门及以下权限
*/
String SYS_DEPT_AND_CHILD = "sys_dept_and_child#30d";
/** /**
* OSS配置 * OSS配置
*/ */

View File

@ -67,11 +67,6 @@ public interface Constants {
*/ */
Integer CAPTCHA_EXPIRATION = 2; Integer CAPTCHA_EXPIRATION = 2;
/**
* 令牌
*/
String TOKEN = "token";
/** /**
* 顶级部门id * 顶级部门id
*/ */

View File

@ -0,0 +1,70 @@
package org.dromara.common.core.constant;
/**
* 用户常量信息
*
* @author Lion Li
*/
public interface SystemConstants {
/**
* 正常状态
*/
String NORMAL = "0";
/**
* 异常状态
*/
String DISABLE = "1";
/**
* 是否为系统默认
*/
String YES = "Y";
/**
* 是否菜单外链
*/
String YES_FRAME = "0";
/**
* 是否菜单外链
*/
String NO_FRAME = "1";
/**
* 菜单类型目录
*/
String TYPE_DIR = "M";
/**
* 菜单类型菜单
*/
String TYPE_MENU = "C";
/**
* 菜单类型按钮
*/
String TYPE_BUTTON = "F";
/**
* Layout组件标识
*/
String LAYOUT = "Layout";
/**
* ParentView组件标识
*/
String PARENT_VIEW = "ParentView";
/**
* InnerLink组件标识
*/
String INNER_LINK = "InnerLink";
/**
* 超级管理员ID
*/
Long SUPER_ADMIN_ID = 1L;
}

View File

@ -7,16 +7,6 @@ package org.dromara.common.core.constant;
*/ */
public interface TenantConstants { public interface TenantConstants {
/**
* 租户正常状态
*/
String NORMAL = "0";
/**
* 租户封禁状态
*/
String DISABLE = "1";
/** /**
* 超级管理员ID * 超级管理员ID
*/ */

View File

@ -1,152 +0,0 @@
package org.dromara.common.core.constant;
/**
* 用户常量信息
*
* @author ruoyi
*/
public interface UserConstants {
/**
* 平台内系统用户的唯一标志
*/
String SYS_USER = "SYS_USER";
/**
* 正常状态
*/
String NORMAL = "0";
/**
* 异常状态
*/
String EXCEPTION = "1";
/**
* 用户正常状态
*/
String USER_NORMAL = "0";
/**
* 用户封禁状态
*/
String USER_DISABLE = "1";
/**
* 角色正常状态
*/
String ROLE_NORMAL = "0";
/**
* 角色封禁状态
*/
String ROLE_DISABLE = "1";
/**
* 部门正常状态
*/
String DEPT_NORMAL = "0";
/**
* 部门停用状态
*/
String DEPT_DISABLE = "1";
/**
* 岗位正常状态
*/
String POST_NORMAL = "0";
/**
* 岗位停用状态
*/
String POST_DISABLE = "1";
/**
* 字典正常状态
*/
String DICT_NORMAL = "0";
/**
* 通用存在标志
*/
String DEL_FLAG_NORMAL = "0";
/**
* 通用删除标志
*/
String DEL_FLAG_REMOVED = "2";
/**
* 是否为系统默认
*/
String YES = "Y";
/**
* 是否菜单外链
*/
String YES_FRAME = "0";
/**
* 是否菜单外链
*/
String NO_FRAME = "1";
/**
* 菜单正常状态
*/
String MENU_NORMAL = "0";
/**
* 菜单停用状态
*/
String MENU_DISABLE = "1";
/**
* 菜单类型目录
*/
String TYPE_DIR = "M";
/**
* 菜单类型菜单
*/
String TYPE_MENU = "C";
/**
* 菜单类型按钮
*/
String TYPE_BUTTON = "F";
/**
* Layout组件标识
*/
String LAYOUT = "Layout";
/**
* ParentView组件标识
*/
String PARENT_VIEW = "ParentView";
/**
* InnerLink组件标识
*/
String INNER_LINK = "InnerLink";
/**
* 用户名长度限制
*/
int USERNAME_MIN_LENGTH = 2;
int USERNAME_MAX_LENGTH = 20;
/**
* 密码长度限制
*/
int PASSWORD_MIN_LENGTH = 5;
int PASSWORD_MAX_LENGTH = 20;
/**
* 超级管理员ID
*/
Long SUPER_ADMIN_ID = 1L;
}

View File

@ -0,0 +1,37 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 部门
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class DeptDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 部门ID
*/
private Long deptId;
/**
* 父部门ID
*/
private Long parentId;
/**
* 部门名称
*/
private String deptName;
}

View File

@ -0,0 +1,46 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 岗位
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class PostDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 岗位ID
*/
private Long postId;
/**
* 部门id
*/
private Long deptId;
/**
* 岗位编码
*/
private String postCode;
/**
* 岗位名称
*/
private String postName;
/**
* 岗位类别编码
*/
private String postCategory;
}

View File

@ -1,8 +1,9 @@
package org.dromara.common.core.domain.model; package org.dromara.common.core.domain.model;
import org.dromara.common.core.domain.dto.RoleDTO;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.dromara.common.core.domain.dto.PostDTO;
import org.dromara.common.core.domain.dto.RoleDTO;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@ -111,6 +112,11 @@ public class LoginUser implements Serializable {
*/ */
private List<RoleDTO> roles; private List<RoleDTO> roles;
/**
* 岗位对象
*/
private List<PostDTO> posts;
/** /**
* 数据权限 当前角色ID * 数据权限 当前角色ID
*/ */

View File

@ -5,8 +5,6 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
import static org.dromara.common.core.constant.UserConstants.*;
/** /**
* 密码登录对象 * 密码登录对象
* *
@ -20,14 +18,14 @@ public class PasswordLoginBody extends LoginBody {
* 用户名 * 用户名
*/ */
@NotBlank(message = "{user.username.not.blank}") @NotBlank(message = "{user.username.not.blank}")
@Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}") @Length(min = 2, max = 20, message = "{user.username.length.valid}")
private String username; private String username;
/** /**
* 用户密码 * 用户密码
*/ */
@NotBlank(message = "{user.password.not.blank}") @NotBlank(message = "{user.password.not.blank}")
@Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}") @Length(min = 5, max = 20, message = "{user.password.length.valid}")
private String password; private String password;
} }

View File

@ -5,8 +5,6 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
import static org.dromara.common.core.constant.UserConstants.*;
/** /**
* 用户注册对象 * 用户注册对象
* *
@ -20,14 +18,14 @@ public class RegisterBody extends LoginBody {
* 用户名 * 用户名
*/ */
@NotBlank(message = "{user.username.not.blank}") @NotBlank(message = "{user.username.not.blank}")
@Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}") @Length(min = 2, max = 20, message = "{user.username.length.valid}")
private String username; private String username;
/** /**
* 用户密码 * 用户密码
*/ */
@NotBlank(message = "{user.password.not.blank}") @NotBlank(message = "{user.password.not.blank}")
@Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}") @Length(min = 5, max = 20, message = "{user.password.length.valid}")
private String password; private String password;
private String userType; private String userType;

View File

@ -0,0 +1,146 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.utils.StringUtils;
/*
* 日期格式
* "yyyy"4位数的年份例如2023年表示为"2023"
* "yy"2位数的年份例如2023年表示为"23"
* "MM"2位数的月份取值范围为01到12例如7月表示为"07"
* "M"不带前导零的月份取值范围为1到12例如7月表示为"7"
* "dd"2位数的日期取值范围为01到31例如22日表示为"22"
* "d"不带前导零的日期取值范围为1到31例如22日表示为"22"
* "EEEE"星期的全名例如星期三表示为"Wednesday"
* "E"星期的缩写例如星期三表示为"Wed"
* "DDD" "D"一年中的第几天取值范围为001到366例如第200天表示为"200"
* 时间格式
* "HH"24小时制的小时数取值范围为00到23例如下午5点表示为"17"
* "hh"12小时制的小时数取值范围为01到12例如下午5点表示为"05"
* "mm"分钟数取值范围为00到59例如30分钟表示为"30"
* "ss"秒数取值范围为00到59例如45秒表示为"45"
* "SSS"毫秒数取值范围为000到999例如123毫秒表示为"123"
*/
/**
* 日期格式与时间格式枚举
*/
@Getter
@AllArgsConstructor
public enum FormatsType {
/**
* 例如2023年表示为"23"
*/
YY("yy"),
/**
* 例如2023年表示为"2023"
*/
YYYY("yyyy"),
/**
* 例例如2023年7月可以表示为 "2023-07"
*/
YYYY_MM("yyyy-MM"),
/**
* 例如日期 "2023年7月22日" 可以表示为 "2023-07-22"
*/
YYYY_MM_DD("yyyy-MM-dd"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分"则可以表示为 "2023-07-22 15:30"
*/
YYYY_MM_DD_HH_MM("yyyy-MM-dd HH:mm"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023-07-22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SS("yyyy-MM-dd HH:mm:ss"),
/**
* 例如下午3点30分45秒表示为 "15:30:45"
*/
HH_MM_SS("HH:mm:ss"),
/**
* 例例如2023年7月可以表示为 "2023/07"
*/
YYYY_MM_SLASH("yyyy/MM"),
/**
* 例如日期 "2023年7月22日" 可以表示为 "2023/07/22"
*/
YYYY_MM_DD_SLASH("yyyy/MM/dd"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023/07/22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SLASH("yyyy/MM/dd HH:mm"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023/07/22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SS_SLASH("yyyy/MM/dd HH:mm:ss"),
/**
* 例例如2023年7月可以表示为 "2023.07"
*/
YYYY_MM_DOT("yyyy.MM"),
/**
* 例如日期 "2023年7月22日" 可以表示为 "2023.07.22"
*/
YYYY_MM_DD_DOT("yyyy.MM.dd"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分"则可以表示为 "2023.07.22 15:30"
*/
YYYY_MM_DD_HH_MM_DOT("yyyy.MM.dd HH:mm"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023.07.22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SS_DOT("yyyy.MM.dd HH:mm:ss"),
/**
* 例如2023年7月可以表示为 "202307"
*/
YYYYMM("yyyyMM"),
/**
* 例如2023年7月22日可以表示为 "20230722"
*/
YYYYMMDD("yyyyMMdd"),
/**
* 例如2023年7月22日下午3点可以表示为 "2023072215"
*/
YYYYMMDDHH("yyyyMMddHH"),
/**
* 例如2023年7月22日下午3点30分可以表示为 "202307221530"
*/
YYYYMMDDHHMM("yyyyMMddHHmm"),
/**
* 例如2023年7月22日下午3点30分45秒可以表示为 "20230722153045"
*/
YYYYMMDDHHMMSS("yyyyMMddHHmmss");
/**
* 时间格式
*/
private final String timeFormat;
public static FormatsType getFormatsType(String str) {
for (FormatsType value : values()) {
if (StringUtils.contains(str, value.getTimeFormat())) {
return value;
}
}
throw new RuntimeException("'FormatsType' not found By " + str);
}
}

View File

@ -1,30 +0,0 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户状态
*
* @author LionLi
*/
@Getter
@AllArgsConstructor
public enum TenantStatus {
/**
* 正常
*/
OK("0", "正常"),
/**
* 停用
*/
DISABLE("1", "停用"),
/**
* 删除
*/
DELETED("2", "删除");
private final String code;
private final String info;
}

View File

@ -1,5 +1,9 @@
package org.dromara.common.core.service; package org.dromara.common.core.service;
import org.dromara.common.core.domain.dto.DeptDTO;
import java.util.List;
/** /**
* 通用 部门服务 * 通用 部门服务
* *
@ -15,4 +19,19 @@ public interface DeptService {
*/ */
String selectDeptNameByIds(String deptIds); String selectDeptNameByIds(String deptIds);
/**
* 根据部门ID查询部门负责人
*
* @param deptId 部门ID用于指定需要查询的部门
* @return 返回该部门的负责人ID
*/
Long selectDeptLeaderById(Long deptId);
/**
* 查询部门
*
* @return 部门列表
*/
List<DeptDTO> selectDeptsByList();
} }

View File

@ -0,0 +1,10 @@
package org.dromara.common.core.service;
/**
* 通用 岗位服务
*
* @author AprilWind
*/
public interface PostService {
}

View File

@ -0,0 +1,10 @@
package org.dromara.common.core.service;
/**
* 通用 角色服务
*
* @author AprilWind
*/
public interface RoleService {
}

View File

@ -82,4 +82,13 @@ public interface UserService {
* @return 用户 * @return 用户
*/ */
List<UserDTO> selectUsersByDeptIds(List<Long> deptIds); List<UserDTO> selectUsersByDeptIds(List<Long> deptIds);
/**
* 通过岗位ID查询用户
*
* @param postIds 岗位ids
* @return 用户
*/
List<UserDTO> selectUsersByPostIds(List<Long> postIds);
} }

View File

@ -3,16 +3,15 @@ package org.dromara.common.core.utils;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import org.dromara.common.core.enums.FormatsType;
import org.dromara.common.core.exception.ServiceException;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDate; import java.time.*;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
import java.util.concurrent.TimeUnit;
/** /**
* 时间工具类 * 时间工具类
@ -21,86 +20,137 @@ import java.util.Date;
*/ */
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DateUtils extends org.apache.commons.lang3.time.DateUtils { public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
public static final String YYYY = "yyyy";
public static final String YYYY_MM = "yyyy-MM";
public static final String YYYY_MM_DD = "yyyy-MM-dd";
public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
private static final String[] PARSE_PATTERNS = { private static final String[] PARSE_PATTERNS = {
"yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
"yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
"yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
/** /**
* 获取当前Date型日期 * 获取当前日期和时间
* *
* @return Date() 当前日期 * @return 当前日期和时间的 Date 对象表示
*/ */
public static Date getNowDate() { public static Date getNowDate() {
return new Date(); return new Date();
} }
/** /**
* 获取当前日期, 默认格式为yyyy-MM-dd * 获取当前日期的字符串表示格式为YYYY-MM-DD
* *
* @return String * @return 当前日期的字符串表示
*/ */
public static String getDate() { public static String getDate() {
return dateTimeNow(YYYY_MM_DD); return dateTimeNow(FormatsType.YYYY_MM_DD);
} }
/**
* 获取当前日期的字符串表示格式为yyyyMMdd
*
* @return 当前日期的字符串表示
*/
public static String getCurrentDate() {
return DateFormatUtils.format(new Date(), FormatsType.YYYYMMDD.getTimeFormat());
}
/**
* 获取当前日期的路径格式字符串格式为"yyyy/MM/dd"
*
* @return 当前日期的路径格式字符串
*/
public static String datePath() {
Date now = new Date();
return DateFormatUtils.format(now, FormatsType.YYYY_MM_DD_SLASH.getTimeFormat());
}
/**
* 获取当前时间的字符串表示格式为YYYY-MM-DD HH:MM:SS
*
* @return 当前时间的字符串表示
*/
public static String getTime() { public static String getTime() {
return dateTimeNow(YYYY_MM_DD_HH_MM_SS); return dateTimeNow(FormatsType.YYYY_MM_DD_HH_MM_SS);
} }
/**
* 获取当前时间的字符串表示格式为 "HH:MM:SS"
*
* @return 当前时间的字符串表示格式为 "HH:MM:SS"
*/
public static String getTimeWithHourMinuteSecond() {
return dateTimeNow(FormatsType.HH_MM_SS);
}
/**
* 获取当前日期和时间的字符串表示格式为YYYYMMDDHHMMSS
*
* @return 当前日期和时间的字符串表示
*/
public static String dateTimeNow() { public static String dateTimeNow() {
return dateTimeNow(YYYYMMDDHHMMSS); return dateTimeNow(FormatsType.YYYYMMDDHHMMSS);
} }
public static String dateTimeNow(final String format) { /**
* 获取当前日期和时间的指定格式的字符串表示
*
* @param format 日期时间格式例如"YYYY-MM-DD HH:MM:SS"
* @return 当前日期和时间的字符串表示
*/
public static String dateTimeNow(final FormatsType format) {
return parseDateToStr(format, new Date()); return parseDateToStr(format, new Date());
} }
public static String dateTime(final Date date) { /**
return parseDateToStr(YYYY_MM_DD, date); * 将指定日期格式化为 YYYY-MM-DD 格式的字符串
*
* @param date 要格式化的日期对象
* @return 格式化后的日期字符串
*/
public static String formatDate(final Date date) {
return parseDateToStr(FormatsType.YYYY_MM_DD, date);
} }
public static String parseDateToStr(final String format, final Date date) { /**
return new SimpleDateFormat(format).format(date); * 将指定日期格式化为 YYYY-MM-DD HH:MM:SS 格式的字符串
*
* @param date 要格式化的日期对象
* @return 格式化后的日期时间字符串
*/
public static String formatDateTime(final Date date) {
return parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, date);
} }
public static Date dateTime(final String format, final String ts) { /**
* 将指定日期按照指定格式进行格式化
*
* @param format 要使用的日期时间格式例如"YYYY-MM-DD HH:MM:SS"
* @param date 要格式化的日期对象
* @return 格式化后的日期时间字符串
*/
public static String parseDateToStr(final FormatsType format, final Date date) {
return new SimpleDateFormat(format.getTimeFormat()).format(date);
}
/**
* 将指定格式的日期时间字符串转换为 Date 对象
*
* @param format 要解析的日期时间格式例如"YYYY-MM-DD HH:MM:SS"
* @param ts 要解析的日期时间字符串
* @return 解析后的 Date 对象
* @throws RuntimeException 如果解析过程中发生异常
*/
public static Date parseDateTime(final FormatsType format, final String ts) {
try { try {
return new SimpleDateFormat(format).parse(ts); return new SimpleDateFormat(format.getTimeFormat()).parse(ts);
} catch (ParseException e) { } catch (ParseException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
/** /**
* 日期路径 即年// 如2018/08/08 * 将对象转换为日期对象
*/ *
public static String datePath() { * @param str 要转换的对象通常是字符串
Date now = new Date(); * @return 转换后的日期对象如果转换失败或输入为null则返回null
return DateFormatUtils.format(now, "yyyy/MM/dd");
}
/**
* 日期路径 即年// 如20180808
*/
public static String dateTime() {
Date now = new Date();
return DateFormatUtils.format(now, "yyyyMMdd");
}
/**
* 日期型字符串转化为日期 格式
*/ */
public static Date parseDate(Object str) { public static Date parseDate(Object str) {
if (str == null) { if (str == null) {
@ -115,6 +165,8 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
/** /**
* 获取服务器启动时间 * 获取服务器启动时间
*
* @return 服务器启动时间的 Date 对象表示
*/ */
public static Date getServerStartDate() { public static Date getServerStartDate() {
long time = ManagementFactory.getRuntimeMXBean().getStartTime(); long time = ManagementFactory.getRuntimeMXBean().getStartTime();
@ -122,35 +174,66 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
} }
/** /**
* 计算相差天数 * 计算两个日期之间的天数差以毫秒为单位
*
* @param date1 第一个日期
* @param date2 第二个日期
* @return 两个日期之间的天数差的绝对值
*/ */
public static int differentDaysByMillisecond(Date date1, Date date2) { public static int differentDaysByMillisecond(Date date1, Date date2) {
return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
} }
/** /**
* 计算两个时间差 * 计算两个日期之间的时间差并以天小时和分钟的格式返回
*
* @param endDate 结束日期
* @param nowDate 当前日期
* @return 表示时间差的字符串格式为"天 小时 分钟"
*/ */
public static String getDatePoor(Date endDate, Date nowDate) { public static String getDatePoor(Date endDate, Date nowDate) {
long nd = 1000 * 24 * 60 * 60; long diffInMillis = endDate.getTime() - nowDate.getTime();
long nh = 1000 * 60 * 60; long day = TimeUnit.MILLISECONDS.toDays(diffInMillis);
long nm = 1000 * 60; long hour = TimeUnit.MILLISECONDS.toHours(diffInMillis) % 24;
// long ns = 1000; long min = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) % 60;
// 获得两个时间的毫秒时间差异 return String.format("%d天 %d小时 %d分钟", day, hour, min);
long diff = endDate.getTime() - nowDate.getTime();
// 计算差多少天
long day = diff / nd;
// 计算差多少小时
long hour = diff % nd / nh;
// 计算差多少分钟
long min = diff % nd % nh / nm;
// 计算差多少秒//输出结果
// long sec = diff % nd % nh % nm / ns;
return day + "" + hour + "小时" + min + "分钟";
} }
/** /**
* 增加 LocalDateTime ==> Date * 计算两个时间点的差值小时分钟当值为0时不显示该单位
*
* @param endDate 结束时间
* @param nowDate 当前时间
* @return 时间差字符串格式为 "x天 x小时 x分钟 x秒"若为 0 则不显示
*/
public static String getTimeDifference(Date endDate, Date nowDate) {
long diffInMillis = endDate.getTime() - nowDate.getTime();
long day = TimeUnit.MILLISECONDS.toDays(diffInMillis);
long hour = TimeUnit.MILLISECONDS.toHours(diffInMillis) % 24;
long min = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) % 60;
long sec = TimeUnit.MILLISECONDS.toSeconds(diffInMillis) % 60;
// 构建时间差字符串条件是值不为0才显示
StringBuilder result = new StringBuilder();
if (day > 0) {
result.append(String.format("%d天 ", day));
}
if (hour > 0) {
result.append(String.format("%d小时 ", hour));
}
if (min > 0) {
result.append(String.format("%d分钟 ", min));
}
if (sec > 0) {
result.append(String.format("%d秒", sec));
}
return result.length() > 0 ? result.toString().trim() : "0秒";
}
/**
* LocalDateTime 对象转换为 Date 对象
*
* @param temporalAccessor 要转换的 LocalDateTime 对象
* @return 转换后的 Date 对象
*/ */
public static Date toDate(LocalDateTime temporalAccessor) { public static Date toDate(LocalDateTime temporalAccessor) {
ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
@ -158,11 +241,46 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
} }
/** /**
* 增加 LocalDate ==> Date * LocalDate 对象转换为 Date 对象
*
* @param temporalAccessor 要转换的 LocalDate 对象
* @return 转换后的 Date 对象
*/ */
public static Date toDate(LocalDate temporalAccessor) { public static Date toDate(LocalDate temporalAccessor) {
LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
return Date.from(zdt.toInstant()); return Date.from(zdt.toInstant());
} }
/**
* 校验日期范围
*
* @param startDate 开始日期
* @param endDate 结束日期
* @param maxValue 最大时间跨度的限制值
* @param unit 时间跨度的单位可选择 "DAYS""HOURS" "MINUTES"
*/
public static void validateDateRange(Date startDate, Date endDate, int maxValue, TimeUnit unit) {
// 校验结束日期不能早于开始日期
if (endDate.before(startDate)) {
throw new ServiceException("结束日期不能早于开始日期");
}
// 计算时间跨度
long diffInMillis = endDate.getTime() - startDate.getTime();
// 根据单位转换时间跨度
long diff = switch (unit) {
case DAYS -> TimeUnit.MILLISECONDS.toDays(diffInMillis);
case HOURS -> TimeUnit.MILLISECONDS.toHours(diffInMillis);
case MINUTES -> TimeUnit.MILLISECONDS.toMinutes(diffInMillis);
default -> throw new IllegalArgumentException("不支持的时间单位");
};
// 校验时间跨度不超过最大限制
if (diff > maxValue) {
throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase());
}
}
} }

View File

@ -0,0 +1,60 @@
package org.dromara.common.core.utils;
import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.function.Function;
/**
* 对象工具类
*
* @author 秋辞未寒
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ObjectUtils extends ObjectUtil {
/**
* 如果对象不为空则获取对象中的某个字段 ObjectUtils.notNullGetter(user, User::getName);
*
* @param obj 对象
* @param func 获取方法
* @return 对象字段
*/
public static <T, E> E notNullGetter(T obj, Function<T, E> func) {
if (isNotNull(obj) && isNotNull(func)) {
return func.apply(obj);
}
return null;
}
/**
* 如果对象不为空则获取对象中的某个字段否则返回默认值
*
* @param obj 对象
* @param func 获取方法
* @param defaultValue 默认值
* @return 对象字段
*/
public static <T, E> E notNullGetter(T obj, Function<T, E> func, E defaultValue) {
if (isNotNull(obj) && isNotNull(func)) {
return func.apply(obj);
}
return defaultValue;
}
/**
* 如果值不为空则返回值否则返回默认值
*
* @param obj 对象
* @param defaultValue 默认值
* @return 对象字段
*/
public static <T> T notNull(T obj, T defaultValue) {
if (isNotNull(obj)) {
return obj;
}
return defaultValue;
}
}

View File

@ -25,7 +25,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* 客户端工具类 * 客户端工具类提供获取请求参数响应处理头部信息等常用操作
* *
* @author ruoyi * @author ruoyi
*/ */
@ -33,52 +33,73 @@ import java.util.Map;
public class ServletUtils extends JakartaServletUtil { public class ServletUtils extends JakartaServletUtil {
/** /**
* 获取String参数 * 获取指定名称的 String 类型的请求参数
*
* @param name 参数名
* @return 参数值
*/ */
public static String getParameter(String name) { public static String getParameter(String name) {
return getRequest().getParameter(name); return getRequest().getParameter(name);
} }
/** /**
* 获取String参数 * 获取指定名称的 String 类型的请求参数若参数不存在则返回默认值
*
* @param name 参数名
* @param defaultValue 默认值
* @return 参数值或默认值
*/ */
public static String getParameter(String name, String defaultValue) { public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue); return Convert.toStr(getRequest().getParameter(name), defaultValue);
} }
/** /**
* 获取Integer参数 * 获取指定名称的 Integer 类型的请求参数
*
* @param name 参数名
* @return 参数值
*/ */
public static Integer getParameterToInt(String name) { public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name)); return Convert.toInt(getRequest().getParameter(name));
} }
/** /**
* 获取Integer参数 * 获取指定名称的 Integer 类型的请求参数若参数不存在则返回默认值
*
* @param name 参数名
* @param defaultValue 默认值
* @return 参数值或默认值
*/ */
public static Integer getParameterToInt(String name, Integer defaultValue) { public static Integer getParameterToInt(String name, Integer defaultValue) {
return Convert.toInt(getRequest().getParameter(name), defaultValue); return Convert.toInt(getRequest().getParameter(name), defaultValue);
} }
/** /**
* 获取Boolean参数 * 获取指定名称的 Boolean 类型的请求参数
*
* @param name 参数名
* @return 参数值
*/ */
public static Boolean getParameterToBool(String name) { public static Boolean getParameterToBool(String name) {
return Convert.toBool(getRequest().getParameter(name)); return Convert.toBool(getRequest().getParameter(name));
} }
/** /**
* 获取Boolean参数 * 获取指定名称的 Boolean 类型的请求参数若参数不存在则返回默认值
*
* @param name 参数名
* @param defaultValue 默认值
* @return 参数值或默认值
*/ */
public static Boolean getParameterToBool(String name, Boolean defaultValue) { public static Boolean getParameterToBool(String name, Boolean defaultValue) {
return Convert.toBool(getRequest().getParameter(name), defaultValue); return Convert.toBool(getRequest().getParameter(name), defaultValue);
} }
/** /**
* 得所有请求参数 * 取所有请求参数 Map 的形式返回
* *
* @param request 请求对象{@link ServletRequest} * @param request 请求对象{@link ServletRequest}
* @return Map * @return 请求参数的 Map键为参数名值为参数值数组
*/ */
public static Map<String, String[]> getParams(ServletRequest request) { public static Map<String, String[]> getParams(ServletRequest request) {
final Map<String, String[]> map = request.getParameterMap(); final Map<String, String[]> map = request.getParameterMap();
@ -86,10 +107,10 @@ public class ServletUtils extends JakartaServletUtil {
} }
/** /**
* 得所有请求参数 * 取所有请求参数 Map 的形式返回值为字符串形式的拼接
* *
* @param request 请求对象{@link ServletRequest} * @param request 请求对象{@link ServletRequest}
* @return Map * @return 请求参数的 Map键为参数名值为拼接后的字符串
*/ */
public static Map<String, String> getParamMap(ServletRequest request) { public static Map<String, String> getParamMap(ServletRequest request) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
@ -100,7 +121,9 @@ public class ServletUtils extends JakartaServletUtil {
} }
/** /**
* 获取request * 获取当前 HTTP 请求对象
*
* @return 当前 HTTP 请求对象
*/ */
public static HttpServletRequest getRequest() { public static HttpServletRequest getRequest() {
try { try {
@ -111,7 +134,9 @@ public class ServletUtils extends JakartaServletUtil {
} }
/** /**
* 获取response * 获取当前 HTTP 响应对象
*
* @return 当前 HTTP 响应对象
*/ */
public static HttpServletResponse getResponse() { public static HttpServletResponse getResponse() {
try { try {
@ -122,12 +147,25 @@ public class ServletUtils extends JakartaServletUtil {
} }
/** /**
* 获取session * 获取当前请求的 HttpSession 对象
* <p>
* 如果当前请求已经关联了一个会话即已经存在有效的 session ID
* 则返回该会话对象如果没有关联会话则会创建一个新的会话对象并返回
* <p>
* HttpSession 用于存储会话级别的数据如用户登录信息购物车内容等
* 可以在多个请求之间共享会话数据
*
* @return 当前请求的 HttpSession 对象
*/ */
public static HttpSession getSession() { public static HttpSession getSession() {
return getRequest().getSession(); return getRequest().getSession();
} }
/**
* 获取当前请求的请求属性
*
* @return {@link ServletRequestAttributes} 请求属性对象
*/
public static ServletRequestAttributes getRequestAttributes() { public static ServletRequestAttributes getRequestAttributes() {
try { try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
@ -137,6 +175,13 @@ public class ServletUtils extends JakartaServletUtil {
} }
} }
/**
* 获取指定请求头的值如果头部为空则返回空字符串
*
* @param request 请求对象
* @param name 头部名称
* @return 头部值
*/
public static String getHeader(HttpServletRequest request, String name) { public static String getHeader(HttpServletRequest request, String name) {
String value = request.getHeader(name); String value = request.getHeader(name);
if (StringUtils.isEmpty(value)) { if (StringUtils.isEmpty(value)) {
@ -145,6 +190,12 @@ public class ServletUtils extends JakartaServletUtil {
return urlDecode(value); return urlDecode(value);
} }
/**
* 获取所有请求头的 Map键为头部名称值为头部值
*
* @param request 请求对象
* @return 请求头的 Map
*/
public static Map<String, String> getHeaders(HttpServletRequest request) { public static Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>(); Map<String, String> map = new LinkedCaseInsensitiveMap<>();
Enumeration<String> enumeration = request.getHeaderNames(); Enumeration<String> enumeration = request.getHeaderNames();
@ -159,7 +210,7 @@ public class ServletUtils extends JakartaServletUtil {
} }
/** /**
* 将字符串渲染到客户端 * 将字符串渲染到客户端 JSON 格式返回
* *
* @param response 渲染对象 * @param response 渲染对象
* @param string 待渲染的字符串 * @param string 待渲染的字符串
@ -176,37 +227,47 @@ public class ServletUtils extends JakartaServletUtil {
} }
/** /**
* 是否是Ajax异步请求 * 判断当前请求是否为 Ajax 异步请求
* *
* @param request * @param request 请求对象
* @return 是否为 Ajax 请求
*/ */
public static boolean isAjaxRequest(HttpServletRequest request) { public static boolean isAjaxRequest(HttpServletRequest request) {
// 判断 Accept 头部是否包含 application/json
String accept = request.getHeader("accept"); String accept = request.getHeader("accept");
if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) { if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
return true; return true;
} }
// 判断 X-Requested-With 头部是否包含 XMLHttpRequest
String xRequestedWith = request.getHeader("X-Requested-With"); String xRequestedWith = request.getHeader("X-Requested-With");
if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) { if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
return true; return true;
} }
// 判断 URI 后缀是否为 .json .xml
String uri = request.getRequestURI(); String uri = request.getRequestURI();
if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) { if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) {
return true; return true;
} }
// 判断请求参数 __ajax 是否为 json xml
String ajax = request.getParameter("__ajax"); String ajax = request.getParameter("__ajax");
return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml"); return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
} }
/**
* 获取客户端 IP 地址
*
* @return 客户端 IP 地址
*/
public static String getClientIP() { public static String getClientIP() {
return getClientIP(getRequest()); return getClientIP(getRequest());
} }
/** /**
* 内容编码 * 内容进行 URL 编码
* *
* @param str 内容 * @param str 内容
* @return 编码后的内容 * @return 编码后的内容
@ -216,7 +277,7 @@ public class ServletUtils extends JakartaServletUtil {
} }
/** /**
* 内容解码 * 内容进行 URL 解码
* *
* @param str 内容 * @param str 内容
* @return 解码后的内容 * @return 解码后的内容

View File

@ -14,18 +14,6 @@ import java.util.concurrent.*;
@Slf4j @Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Threads { public class Threads {
/**
* sleep等待,单位为毫秒
*/
public static void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
return;
}
}
/** /**
* 停止线程池 * 停止线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.

View File

@ -43,6 +43,23 @@ public class TreeBuildUtils extends TreeUtil {
return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser); return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
} }
/**
* 构建树形结构
*
* @param <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param parentId 顶级节点
* @param list 节点列表其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser);
}
/** /**
* 获取节点列表中所有节点的叶子节点 * 获取节点列表中所有节点的叶子节点
* *

View File

@ -0,0 +1,48 @@
package org.dromara.common.core.validate.enumd;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义枚举校验
*
* @author 秋辞未寒
* @date 2024-12-09
*/
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(EnumPattern.List.class) // 允许在同一元素上多次使用该注解
@Constraint(validatedBy = {EnumPatternValidator.class})
public @interface EnumPattern {
/**
* 需要校验的枚举类型
*/
Class<? extends Enum<?>> type();
/**
* 枚举类型校验值字段名称
* 需确保该字段实现了 getter 方法
*/
String fieldName();
String message() default "输入值不在枚举范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@interface List {
EnumPattern[] value();
}
}

View File

@ -0,0 +1,37 @@
package org.dromara.common.core.validate.enumd;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
/**
* 自定义枚举校验注解实现
*
* @author 秋辞未寒
* @date 2024-12-09
*/
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
private EnumPattern annotation;;
@Override
public void initialize(EnumPattern annotation) {
ConstraintValidator.super.initialize(annotation);
this.annotation = annotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isNotBlank(value)) {
String fieldName = annotation.fieldName();
for (Object e : annotation.type().getEnumConstants()) {
if (value.equals(ReflectUtils.invokeGetter(e, fieldName))) {
return true;
}
}
}
return false;
}
}

View File

@ -1,11 +1,11 @@
package org.dromara.common.encrypt.core; package org.dromara.common.encrypt.core;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.io.Resources; import org.apache.ibatis.io.Resources;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.EncryptField; import org.dromara.common.encrypt.annotation.EncryptField;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
@ -58,10 +58,7 @@ public class EncryptorManager {
* 获取类加密字段缓存 * 获取类加密字段缓存
*/ */
public Set<Field> getFieldCache(Class<?> sourceClazz) { public Set<Field> getFieldCache(Class<?> sourceClazz) {
if (ObjectUtil.isNotNull(fieldCache)) { return ObjectUtils.notNullGetter(fieldCache, f -> f.get(sourceClazz));
return fieldCache.get(sourceClazz);
}
return null;
} }
/** /**

View File

@ -19,10 +19,12 @@ import java.util.Map;
* @author 老马 * @author 老马
*/ */
public class EncryptUtils { public class EncryptUtils {
/** /**
* 公钥 * 公钥
*/ */
public static final String PUBLIC_KEY = "publicKey"; public static final String PUBLIC_KEY = "publicKey";
/** /**
* 私钥 * 私钥
*/ */
@ -51,7 +53,7 @@ public class EncryptUtils {
/** /**
* AES加密 * AES加密
* *
* @param data 密数据 * @param data 密数据
* @param password 秘钥字符串 * @param password 秘钥字符串
* @return 加密后字符串, 采用Base64编码 * @return 加密后字符串, 采用Base64编码
*/ */
@ -70,7 +72,7 @@ public class EncryptUtils {
/** /**
* AES加密 * AES加密
* *
* @param data 密数据 * @param data 密数据
* @param password 秘钥字符串 * @param password 秘钥字符串
* @return 加密后字符串, 采用Hex编码 * @return 加密后字符串, 采用Hex编码
*/ */
@ -208,7 +210,7 @@ public class EncryptUtils {
/** /**
* sm2私钥解密 * sm2私钥解密
* *
* @param data 密数据 * @param data 密数据
* @param privateKey 私钥 * @param privateKey 私钥
* @return 解密后字符串 * @return 解密后字符串
*/ */
@ -266,7 +268,7 @@ public class EncryptUtils {
/** /**
* rsa私钥解密 * rsa私钥解密
* *
* @param data 密数据 * @param data 密数据
* @param privateKey 私钥 * @param privateKey 私钥
* @return 解密后字符串 * @return 解密后字符串
*/ */

View File

@ -0,0 +1,24 @@
package org.dromara.common.excel.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 批注
* @author guzhouyanyu
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelNotation {
/**
* col index
*/
int index() default -1;
/**
* 批注内容
*/
String value() default "";
}

View File

@ -0,0 +1,26 @@
package org.dromara.common.excel.annotation;
import org.apache.poi.ss.usermodel.IndexedColors;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 是否必填
* @author guzhouyanyu
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelRequired {
/**
* col index
*/
int index() default -1;
/**
* 字体颜色
*/
IndexedColors fontColor() default IndexedColors.RED;
}

View File

@ -55,6 +55,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
* 下拉可选项 * 下拉可选项
*/ */
private final List<DropDownOptions> dropDownOptions; private final List<DropDownOptions> dropDownOptions;
private final DictService dictService;
/** /**
* 当前单选进度 * 当前单选进度
*/ */
@ -63,7 +64,6 @@ public class ExcelDownHandler implements SheetWriteHandler {
* 当前联动选择进度 * 当前联动选择进度
*/ */
private int currentLinkedOptionsSheetIndex; private int currentLinkedOptionsSheetIndex;
private final DictService dictService;
public ExcelDownHandler(List<DropDownOptions> options) { public ExcelDownHandler(List<DropDownOptions> options) {
this.dropDownOptions = options; this.dropDownOptions = options;
@ -139,8 +139,8 @@ public class ExcelDownHandler implements SheetWriteHandler {
} else if (everyOptions.getOptions().size() > 10) { } else if (everyOptions.getOptions().size() > 10) {
// 当一级选项参数个数大于10使用额外表的形式 // 当一级选项参数个数大于10使用额外表的形式
dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions()); dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
} else if (everyOptions.getOptions().size() != 0) { } else {
// 当一级选项个数不为空使用默认形式 // 否则使用默认形式
dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions()); dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
} }
}); });
@ -171,10 +171,24 @@ public class ExcelDownHandler implements SheetWriteHandler {
Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName)); Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
// 将下拉表隐藏 // 将下拉表隐藏
workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true); workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
// 完善横向的一级选项数据 // 选项数据
List<String> firstOptions = options.getOptions(); List<String> firstOptions = options.getOptions();
Map<String, List<String>> secoundOptionsMap = options.getNextOptions(); Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
// 采用按行填充数据的方式避免EasyExcel出现数据无法写入的问题
// Attempting to write a row in the range that is already written to disk
// 使用ArrayList记载数据防止乱序
List<String> columnNames = new ArrayList<>();
// 写入第一行即第一级的数据
Row firstRow = linkedOptionsDataSheet.createRow(0);
for (int columnIndex = 0; columnIndex < firstOptions.size(); columnIndex++) {
String columnName = firstOptions.get(columnIndex);
firstRow.createCell(columnIndex)
.setCellValue(columnName);
columnNames.add(columnName);
}
// 创建名称管理器 // 创建名称管理器
Name name = workbook.createName(); Name name = workbook.createName();
// 设置名称管理器的别名 // 设置名称管理器的别名
@ -190,28 +204,12 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 设置数据校验为序列模式引用的是名称管理器中的别名 // 设置数据校验为序列模式引用的是名称管理器中的别名
this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName)); this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) { // 创建二级选项的名称管理器
// 先提取主表中一级下拉的列名 for (int columIndex = 0; columIndex < columnNames.size(); columIndex++) {
// 列名
String firstOptionsColumnName = getExcelColumnName(columIndex); String firstOptionsColumnName = getExcelColumnName(columIndex);
// 一次循环是每一个一级选项 // 对应的一级值
int finalI = columIndex; String thisFirstOptionsValue = columnNames.get(columIndex);
// 本次循环的一级选项值
String thisFirstOptionsValue = firstOptions.get(columIndex);
// 创建第一行的数据
Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
// 如果不存在则创建第一行
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
// 第一行当前列
.createCell(columIndex)
// 设置值为当前一级选项值
.setCellValue(thisFirstOptionsValue);
// 第二行开始设置第二级别选项参数
List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
if (CollUtil.isEmpty(secondOptions)) {
// 必须保证至少有一个关联选项否则将导致Excel解析错误
secondOptions = Collections.singletonList("暂无_0");
}
// 以该一级选项值创建子名称管理器 // 以该一级选项值创建子名称管理器
Name sonName = workbook.createName(); Name sonName = workbook.createName();
@ -222,7 +220,9 @@ public class ExcelDownHandler implements SheetWriteHandler {
linkedOptionsSheetName, linkedOptionsSheetName,
firstOptionsColumnName, firstOptionsColumnName,
firstOptionsColumnName, firstOptionsColumnName,
secondOptions.size() + 1 // 二级选项存在则设置为(选项个数+1)否则设置为2行
Math.max(Optional.ofNullable(secoundOptionsMap.get(thisFirstOptionsValue))
.orElseGet(ArrayList::new).size(), 1) + 1
); );
// 设置名称管理器的引用位置 // 设置名称管理器的引用位置
sonName.setRefersToFormula(sonFunction); sonName.setRefersToFormula(sonFunction);
@ -235,25 +235,51 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 二级只能主表每一行的每一列添加二级校验 // 二级只能主表每一行的每一列添加二级校验
markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction)); markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
} }
}
for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) { // 将二级数据处理为按行区分
// 从第二行开始填充二级选项 Map<Integer, List<String>> columnValueMap = new HashMap<>();
int finalRowIndex = rowIndex + 1; int currentRow = 1;
int finalColumIndex = columIndex; while (currentRow >= 0) {
boolean flag = false;
Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex)) List<String> rowData = new ArrayList<>();
// 没有则创建 for (String columnName : columnNames) {
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex)); List<String> data = secoundOptionsMap.get(columnName);
Optional if (CollUtil.isEmpty(data)) {
// 在本级一级选项所在的列 // 添加空字符串填充位置
.ofNullable(row.getCell(finalColumIndex)) rowData.add(" ");
// 不存在则创建 continue;
.orElseGet(() -> row.createCell(finalColumIndex)) }
// 设置二级选项值 // 取第一个
.setCellValue(secondOptions.get(rowIndex)); String str = data.get(0);
rowData.add(str);
// 通过移除的方式避免重复
data.remove(0);
// 设置可以继续
flag = true;
}
columnValueMap.put(currentRow, rowData);
// 可以继续则增加行数否则置为负数跳出循环
if (flag) {
currentRow++;
} else {
currentRow = -1;
} }
} }
// 填充第二级选项数据
columnValueMap.forEach((rowIndex, rowValues) -> {
Row row = linkedOptionsDataSheet.createRow(rowIndex);
for (int columnIndex = 0; columnIndex < rowValues.size(); columnIndex++) {
String rowValue = rowValues.get(columnIndex);
// 填充位置的部分不渲染
if (StrUtil.isNotBlank(rowValue)) {
row.createCell(columnIndex)
.setCellValue(rowValue);
}
}
});
currentLinkedOptionsSheetIndex++; currentLinkedOptionsSheetIndex++;
} }

View File

@ -0,0 +1,135 @@
package org.dromara.common.excel.handler;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.metadata.data.DataFormatData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.StyleUtil;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.excel.annotation.ExcelNotation;
import org.dromara.common.excel.annotation.ExcelRequired;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* 批注必填
*
* @author guzhouyanyu
*/
public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
/**
* 批注
*/
private final Map<Integer, String> notationMap;
/**
* 头列字体颜色
*/
private final Map<Integer, Short> headColumnMap;
public DataWriteHandler(Class<?> clazz) {
notationMap = getNotationMap(clazz);
headColumnMap = getRequiredMap(clazz);
}
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) {
return;
}
WriteCellData<?> cellData = context.getFirstCellData();
WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
DataFormatData dataFormatData = new DataFormatData();
// 单元格设置为文本格式
dataFormatData.setIndex((short) 49);
writeCellStyle.setDataFormatData(dataFormatData);
if (context.getHead()) {
Cell cell = context.getCell();
WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder();
Sheet sheet = writeSheetHolder.getSheet();
Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
Drawing<?> drawing = sheet.createDrawingPatriarch();
// 设置标题字体样式
WriteFont headWriteFont = new WriteFont();
// 加粗
headWriteFont.setBold(true);
if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getColumnIndex())) {
// 设置字体颜色
headWriteFont.setColor(headColumnMap.get(cell.getColumnIndex()));
}
writeCellStyle.setWriteFont(headWriteFont);
CellStyle cellStyle = StyleUtil.buildCellStyle(workbook, null, writeCellStyle);
cell.setCellStyle(cellStyle);
if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getColumnIndex())) {
// 批注内容
String notationContext = notationMap.get(cell.getColumnIndex());
// 创建绘图对象
Comment comment = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), 0, (short) 5, 5));
comment.setString(new XSSFRichTextString(notationContext));
cell.setCellComment(comment);
}
}
}
/**
* 获取必填列
*/
private static Map<Integer, Short> getRequiredMap(Class<?> clazz) {
Map<Integer, Short> requiredMap = new HashMap<>();
Field[] fields = clazz.getDeclaredFields();
// 检查 fields 数组是否为空
if (fields.length == 0) {
return requiredMap;
}
Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
for (int i = 0; i < filteredFields.length; i++) {
Field field = filteredFields[i];
if (!field.isAnnotationPresent(ExcelRequired.class)) {
continue;
}
ExcelRequired excelRequired = field.getAnnotation(ExcelRequired.class);
int columnIndex = excelRequired.index() == -1 ? i : excelRequired.index();
requiredMap.put(columnIndex, excelRequired.fontColor().getIndex());
}
return requiredMap;
}
/**
* 获取批注
*/
private static Map<Integer, String> getNotationMap(Class<?> clazz) {
Map<Integer, String> notationMap = new HashMap<>();
Field[] fields = clazz.getDeclaredFields();
// 检查 fields 数组是否为空
if (fields.length == 0) {
return notationMap;
}
Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
for (int i = 0; i < filteredFields.length; i++) {
Field field = filteredFields[i];
if (!field.isAnnotationPresent(ExcelNotation.class)) {
continue;
}
ExcelNotation excelNotation = field.getAnnotation(ExcelNotation.class);
int columnIndex = excelNotation.index() == -1 ? i : excelNotation.index();
notationMap.put(columnIndex, excelNotation.value());
}
return notationMap;
}
}

View File

@ -18,6 +18,7 @@ import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils; import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.convert.ExcelBigNumberConvert; import org.dromara.common.excel.convert.ExcelBigNumberConvert;
import org.dromara.common.excel.core.*; import org.dromara.common.excel.core.*;
import org.dromara.common.excel.handler.DataWriteHandler;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -201,6 +202,7 @@ public class ExcelUtil {
.excludeColumnFieldNames(excludeColumnFieldNames) .excludeColumnFieldNames(excludeColumnFieldNames)
// 大数值自动转换 防止失真 // 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
.registerWriteHandler(new DataWriteHandler(list.get(0).getClass()))
.sheet(sheetName); .sheet(sheetName);
if (merge) { if (merge) {
// 合并处理器 // 合并处理器
@ -221,7 +223,7 @@ public class ExcelUtil {
* @param data 模板需要的数据 * @param data 模板需要的数据
* @param response 响应体 * @param response 响应体
*/ */
public static void exportTemplate(List<Object> data, String filename, String templatePath, HttpServletResponse response) { public static <T> void exportTemplate(List<T> data, String filename, String templatePath, HttpServletResponse response) {
try { try {
resetResponse(filename, response); resetResponse(filename, response);
ServletOutputStream os = response.getOutputStream(); ServletOutputStream os = response.getOutputStream();
@ -240,20 +242,21 @@ public class ExcelUtil {
* @param data 模板需要的数据 * @param data 模板需要的数据
* @param os 输出流 * @param os 输出流
*/ */
public static void exportTemplate(List<Object> data, String templatePath, OutputStream os) { public static <T> void exportTemplate(List<T> data, String templatePath, OutputStream os) {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
ClassPathResource templateResource = new ClassPathResource(templatePath); ClassPathResource templateResource = new ClassPathResource(templatePath);
ExcelWriter excelWriter = EasyExcel.write(os) ExcelWriter excelWriter = EasyExcel.write(os)
.withTemplate(templateResource.getStream()) .withTemplate(templateResource.getStream())
.autoCloseStream(false) .autoCloseStream(false)
// 大数值自动转换 防止失真 // 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
.registerWriteHandler(new DataWriteHandler(data.get(0).getClass()))
.build(); .build();
WriteSheet writeSheet = EasyExcel.writerSheet().build(); WriteSheet writeSheet = EasyExcel.writerSheet().build();
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
// 单表多数据导出 模板格式为 {.属性} // 单表多数据导出 模板格式为 {.属性}
for (Object d : data) { for (T d : data) {
excelWriter.fill(d, writeSheet); excelWriter.fill(d, writeSheet);
} }
excelWriter.finish(); excelWriter.finish();
@ -309,6 +312,9 @@ public class ExcelUtil {
* @param os 输出流 * @param os 输出流
*/ */
public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) { public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
ClassPathResource templateResource = new ClassPathResource(templatePath); ClassPathResource templateResource = new ClassPathResource(templatePath);
ExcelWriter excelWriter = EasyExcel.write(os) ExcelWriter excelWriter = EasyExcel.write(os)
.withTemplate(templateResource.getStream()) .withTemplate(templateResource.getStream())
@ -317,9 +323,6 @@ public class ExcelUtil {
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
.build(); .build();
WriteSheet writeSheet = EasyExcel.writerSheet().build(); WriteSheet writeSheet = EasyExcel.writerSheet().build();
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
for (Map.Entry<String, Object> map : data.entrySet()) { for (Map.Entry<String, Object> map : data.entrySet()) {
// 设置列表后续还有数据 // 设置列表后续还有数据
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
@ -343,6 +346,9 @@ public class ExcelUtil {
* @param os 输出流 * @param os 输出流
*/ */
public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) { public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
ClassPathResource templateResource = new ClassPathResource(templatePath); ClassPathResource templateResource = new ClassPathResource(templatePath);
ExcelWriter excelWriter = EasyExcel.write(os) ExcelWriter excelWriter = EasyExcel.write(os)
.withTemplate(templateResource.getStream()) .withTemplate(templateResource.getStream())
@ -350,9 +356,6 @@ public class ExcelUtil {
// 大数值自动转换 防止失真 // 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
.build(); .build();
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
for (int i = 0; i < data.size(); i++) { for (int i = 0; i < data.size(); i++) {
WriteSheet writeSheet = EasyExcel.writerSheet(i).build(); WriteSheet writeSheet = EasyExcel.writerSheet(i).build();
for (Map.Entry<String, Object> map : data.get(i).entrySet()) { for (Map.Entry<String, Object> map : data.get(i).entrySet()) {

View File

@ -32,7 +32,7 @@ public class BigNumberSerializer extends NumberSerializer {
@Override @Override
public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException { public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 超出范围 序列化字符串 // 超出范围 序列化字符串
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
super.serialize(value, gen, provider); super.serialize(value, gen, provider);
} else { } else {

View File

@ -100,7 +100,7 @@ public class LogAspect {
if (e != null) { if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal()); operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 3800));
} }
// 设置方法名称 // 设置方法名称
String className = joinPoint.getTarget().getClass().getName(); String className = joinPoint.getTarget().getClass().getName();
@ -146,7 +146,7 @@ public class LogAspect {
} }
// 是否需要保存response参数和值 // 是否需要保存response参数和值
if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) { if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000)); operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 3800));
} }
} }
@ -161,11 +161,11 @@ public class LogAspect {
String requestMethod = operLog.getRequestMethod(); String requestMethod = operLog.getRequestMethod();
if (MapUtil.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) { if (MapUtil.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 2000)); operLog.setOperParam(StringUtils.substring(params, 0, 3800));
} else { } else {
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES); MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, excludeParamNames); MapUtil.removeAny(paramsMap, excludeParamNames);
operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000)); operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 3800));
} }
} }

View File

@ -43,7 +43,13 @@ public class MailProperties {
private String pass; private String pass;
/** /**
* 发送方遵循RFC-822标准 * 发送方遵循RFC-822标准<br>
* 发件人可以是以下形式
*
* <pre>
* 1. user@xxx.xx
* 2. name &lt;user@xxx.xx&gt;
* </pre>
*/ */
private String from; private String from;

View File

@ -37,6 +37,11 @@
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!-- sql性能分析插件 --> <!-- sql性能分析插件 -->
<dependency> <dependency>
<groupId>p6spy</groupId> <groupId>p6spy</groupId>

View File

@ -0,0 +1,50 @@
package org.dromara.common.mybatis.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
/**
* 数据权限处理
*
* @author Lion Li
*/
@Slf4j
@Aspect
public class DataPermissionAspect {
/**
* 处理请求前执行
*/
@Before(value = "@annotation(dataPermission)")
public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) {
DataPermissionHelper.setPermission(dataPermission);
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(dataPermission)")
public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) {
DataPermissionHelper.removePermission();
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(dataPermission)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) {
DataPermissionHelper.removePermission();
}
}

View File

@ -2,6 +2,7 @@ package org.dromara.common.mybatis.config;
import cn.hutool.core.net.NetUtil; import cn.hutool.core.net.NetUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator; import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
@ -10,8 +11,10 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.dromara.common.core.factory.YmlPropertySourceFactory; import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.mybatis.aspect.DataPermissionAspect;
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler; import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
import org.dromara.common.mybatis.handler.MybatisExceptionHandler; import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor; import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
@ -54,6 +57,14 @@ public class MybatisPlusConfig {
return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage")); return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
} }
/**
* 数据权限切面处理器
*/
@Bean
public DataPermissionAspect dataPermissionAspect() {
return new DataPermissionAspect();
}
/** /**
* 分页插件自动识别数据库类型 * 分页插件自动识别数据库类型
*/ */
@ -96,6 +107,14 @@ public class MybatisPlusConfig {
return new MybatisExceptionHandler(); return new MybatisExceptionHandler();
} }
/**
* 初始化表对象处理器
*/
@Bean
public PostInitTableInfoHandler postInitTableInfoHandler() {
return new PlusPostInitTableInfoHandler();
}
/** /**
* PaginationInnerInterceptor 分页插件自动识别数据库类型 * PaginationInnerInterceptor 分页插件自动识别数据库类型
* https://baomidou.com/pages/97710a/ * https://baomidou.com/pages/97710a/

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; import lombok.Data;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
@ -113,8 +114,14 @@ public class PageQuery implements Serializable {
return list; return list;
} }
@JsonIgnore
public Integer getFirstNum() { public Integer getFirstNum() {
return (pageNum - 1) * pageSize; return (pageNum - 1) * pageSize;
} }
public PageQuery(Integer pageSize, Integer pageNum) {
this.pageSize = pageSize;
this.pageNum = pageNum;
}
} }

View File

@ -48,7 +48,12 @@ public enum DataScopeType {
/** /**
* 仅本人数据权限 * 仅本人数据权限
*/ */
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "); SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "),
/**
* 部门及以下或本人数据权限
*/
DEPT_AND_CHILD_OR_SELF("6", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) OR #{#userName} = #{#user.userId} ", " 1 = 0 ");
private final String code; private final String code;

View File

@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.MetaObject;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
@ -31,8 +32,7 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
try { try {
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) { if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
// 获取当前时间作为创建时间和更新时间如果创建时间不为空则使用创建时间否则使用当前时间 // 获取当前时间作为创建时间和更新时间如果创建时间不为空则使用创建时间否则使用当前时间
Date current = ObjectUtil.isNotNull(baseEntity.getCreateTime()) Date current = ObjectUtils.notNull(baseEntity.getCreateTime(), new Date());
? baseEntity.getCreateTime() : new Date();
baseEntity.setCreateTime(current); baseEntity.setCreateTime(current);
baseEntity.setUpdateTime(current); baseEntity.setUpdateTime(current);
@ -44,8 +44,7 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
// 填充创建人更新人和创建部门信息 // 填充创建人更新人和创建部门信息
baseEntity.setCreateBy(userId); baseEntity.setCreateBy(userId);
baseEntity.setUpdateBy(userId); baseEntity.setUpdateBy(userId);
baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept()) baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId()));
? baseEntity.getCreateDept() : loginUser.getDeptId());
} }
} }
} else { } else {

View File

@ -1,6 +1,5 @@
package org.dromara.common.mybatis.handler; package org.dromara.common.mybatis.handler;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
@ -37,11 +36,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.ClassUtils; 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.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
@ -55,7 +50,7 @@ import java.util.function.Function;
public class PlusDataPermissionHandler { public class PlusDataPermissionHandler {
/** /**
* 方法或类(名称) 注解的映射关系缓存 * 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行)
*/ */
private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>(); private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
@ -87,28 +82,24 @@ public class PlusDataPermissionHandler {
* @return 数据过滤条件的 SQL 片段 * @return 数据过滤条件的 SQL 片段
*/ */
public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) { public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
// 未登录则不过滤数据防止租户未登录 查询数据异常
if (!StpUtil.isLogin()) {
return where;
}
// 获取数据权限配置
DataPermission dataPermission = getDataPermission(mappedStatementId);
// 获取当前登录用户信息
LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) {
currentUser = LoginHelper.getLoginUser();
DataPermissionHelper.setVariable("user", currentUser);
}
// 如果是超级管理员或租户管理员则不过滤数据
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
return where;
}
// 构造数据过滤条件的 SQL 片段
String dataFilterSql = buildDataFilter(dataPermission, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
try { try {
// 获取数据权限配置
DataPermission dataPermission = getDataPermission(mappedStatementId);
// 获取当前登录用户信息
LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) {
currentUser = LoginHelper.getLoginUser();
DataPermissionHelper.setVariable("user", currentUser);
}
// 如果是超级管理员或租户管理员则不过滤数据
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
return where;
}
// 构造数据过滤条件的 SQL 片段
String dataFilterSql = buildDataFilter(dataPermission, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql); Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
// 数据权限使用单独的括号 防止与其他条件冲突 // 数据权限使用单独的括号 防止与其他条件冲突
ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(expression); ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(expression);
@ -119,6 +110,8 @@ public class PlusDataPermissionHandler {
} }
} catch (JSQLParserException e) { } catch (JSQLParserException e) {
throw new ServiceException("数据权限解析异常 => " + e.getMessage()); throw new ServiceException("数据权限解析异常 => " + e.getMessage());
} finally {
DataPermissionHelper.removePermission();
} }
} }
@ -153,30 +146,37 @@ public class PlusDataPermissionHandler {
return ""; return "";
} }
boolean isSuccess = false; boolean isSuccess = false;
List<String> keys = new ArrayList<>();
for (DataColumn dataColumn : dataPermission.value()) { for (DataColumn dataColumn : dataPermission.value()) {
if (dataColumn.key().length != dataColumn.value().length) { if (dataColumn.key().length != dataColumn.value().length) {
throw new ServiceException("角色数据范围异常 => key与value长度不匹配"); throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
} }
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
}
keys.addAll(Arrays.stream(dataColumn.key()).map(key -> "#" + key).toList());
}
for (DataColumn dataColumn : dataPermission.value()) {
// 不包含 key 变量 则不处理 // 不包含 key 变量 则不处理
if (!StringUtils.containsAny(type.getSqlTemplate(), if (!StringUtils.containsAny(type.getSqlTemplate(), keys.toArray(String[]::new))) {
Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
)) {
continue; continue;
} }
// 包含权限标识符 这直接跳过 // 包含权限标识符 这直接跳过
if (StringUtils.isNotBlank(dataColumn.permission()) && if (StringUtils.isNotBlank(dataColumn.permission()) &&
CollUtil.contains(user.getMenuPermission(), dataColumn.permission()) CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
) { ) {
// 修复多角色与权限标识符共用问题 https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IB4CS4
conditions.add(joinStr + " 1 = 1 ");
isSuccess = true; isSuccess = true;
continue; continue;
} }
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
}
// 忽略数据权限 防止spel表达式内有其他sql查询导致死循环调用
String sql = DataPermissionHelper.ignore(() ->
parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class)
);
// 解析sql模板并填充 // 解析sql模板并填充
String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
conditions.add(joinStr + sql); conditions.add(joinStr + sql);
isSuccess = true; isSuccess = true;
} }
@ -217,7 +217,10 @@ public class PlusDataPermissionHandler {
// 获取资源对应的类对象 // 获取资源对应的类对象
Class<?> clazz = Resources.classForName(classMetadata.getClassName()); Class<?> clazz = Resources.classForName(classMetadata.getClassName());
// 查找类中的特定注解 // 查找类中的特定注解
findAnnotation(clazz); if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
DataPermission dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
dataPermissionCacheMap.put(clazz.getName(), dataPermission);
}
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -225,29 +228,6 @@ public class PlusDataPermissionHandler {
} }
} }
/**
* 在指定的类中查找特定的注解 DataPermission并将带有这个注解的方法或类存储到 dataPermissionCacheMap
*
* @param clazz 要查找的类
*/
private void findAnnotation(Class<?> clazz) {
DataPermission dataPermission;
for (Method method : clazz.getMethods()) {
if (method.isDefault() || method.isVarArgs()) {
continue;
}
String mappedStatementId = clazz.getName() + "." + method.getName();
if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
dataPermissionCacheMap.put(mappedStatementId, dataPermission);
}
}
if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
dataPermissionCacheMap.put(clazz.getName(), dataPermission);
}
}
/** /**
* 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象 * 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
* *
@ -255,9 +235,9 @@ public class PlusDataPermissionHandler {
* @return DataPermission 注解对象如果不存在则返回 null * @return DataPermission 注解对象如果不存在则返回 null
*/ */
public DataPermission getDataPermission(String mapperId) { public DataPermission getDataPermission(String mapperId) {
// 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象 // 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象
if (dataPermissionCacheMap.containsKey(mapperId)) { if (DataPermissionHelper.getPermission() != null) {
return dataPermissionCacheMap.get(mapperId); return DataPermissionHelper.getPermission();
} }
// 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象则尝试使用类名作为键查找 // 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象则尝试使用类名作为键查找
String clazzName = mapperId.substring(0, mapperId.lastIndexOf(".")); String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
@ -276,4 +256,5 @@ public class PlusDataPermissionHandler {
public boolean invalid(String mapperId) { public boolean invalid(String mapperId) {
return getDataPermission(mapperId) == null; return getDataPermission(mapperId) == null;
} }
} }

View File

@ -0,0 +1,27 @@
package org.dromara.common.mybatis.handler;
import cn.hutool.core.convert.Convert;
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.session.Configuration;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
/**
* 修改表信息初始化方式
* 目前用于全局修改是否使用逻辑删除
*
* @author Lion Li
*/
public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
@Override
public void postTableInfo(TableInfo tableInfo, Configuration configuration) {
String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
// 只有关闭时 统一设置false 为true时mp自动判断不处理
if (!Convert.toBool(flag)) {
ReflectUtils.setFieldValue(tableInfo, "withLogicDelete", false);
}
}
}

View File

@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.reflect.ReflectUtils; import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.mybatis.annotation.DataPermission;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -29,6 +30,33 @@ public class DataPermissionHelper {
private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new); private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
private static final ThreadLocal<DataPermission> PERMISSION_CACHE = new ThreadLocal<>();
/**
* 获取当前执行mapper权限注解
*
* @return 返回当前执行mapper权限注解
*/
public static DataPermission getPermission() {
return PERMISSION_CACHE.get();
}
/**
* 设置当前执行mapper权限注解
*
* @param dataPermission 数据权限注解
*/
public static void setPermission(DataPermission dataPermission) {
PERMISSION_CACHE.set(dataPermission);
}
/**
* 删除当前执行mapper权限注解
*/
public static void removePermission() {
PERMISSION_CACHE.remove();
}
/** /**
* 从上下文中获取指定键的变量值并将其转换为指定的类型 * 从上下文中获取指定键的变量值并将其转换为指定的类型
* *

View File

@ -24,8 +24,8 @@ mybatis-plus:
# 主键类型 # 主键类型
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID # AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
idType: ASSIGN_ID idType: ASSIGN_ID
# 逻辑已删除值(框架表均使用此值 禁止随意修改) # 逻辑已删除值(可按需求随意修改)
logicDeleteValue: 2 logicDeleteValue: 1
# 逻辑未删除值 # 逻辑未删除值
logicNotDeleteValue: 0 logicNotDeleteValue: 0
insertStrategy: NOT_NULL insertStrategy: NOT_NULL

View File

@ -9,7 +9,6 @@ import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.oss.constant.OssConstant; import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.entity.UploadResult; import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.enumd.AccessPolicyType; 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.exception.OssException;
import org.dromara.common.oss.properties.OssProperties; import org.dromara.common.oss.properties.OssProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
@ -22,8 +21,6 @@ import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration; import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration;
import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.GetObjectResponse;
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.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.*; import software.amazon.awssdk.transfer.s3.model.*;
@ -115,8 +112,6 @@ public class OssClient {
.serviceConfiguration(config) .serviceConfiguration(config)
.build(); .build();
// 创建存储桶
createBucket();
} catch (Exception e) { } catch (Exception e) {
if (e instanceof OssException) { if (e instanceof OssException) {
throw e; throw e;
@ -125,43 +120,6 @@ public class OssClient {
} }
} }
/**
* 同步创建存储桶
* 如果存储桶不存在会进行创建如果存储桶存在不执行任何操作
*
* @throws OssException 当创建存储桶时发生异常时抛出
*/
public void createBucket() {
String bucketName = properties.getBucketName();
try {
// 尝试获取存储桶的信息
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() + "]");
}
}
}
/** /**
* 上传文件到 Amazon S3并返回上传结果 * 上传文件到 Amazon S3并返回上传结果
* *
@ -529,77 +487,4 @@ public class OssClient {
return AccessPolicyType.getByType(properties.getAccessPolicy()); return AccessPolicyType.getByType(properties.getAccessPolicy());
} }
/**
* 生成 AWS S3 存储桶访问策略
*
* @param bucketName 存储桶
* @param policyType 桶策略类型
* @return 符合 AWS S3 存储桶访问策略格式的字符串
*/
private static String getPolicy(String bucketName, PolicyType policyType) {
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);
}
} }

View File

@ -17,17 +17,17 @@ public enum AccessPolicyType {
/** /**
* private * private
*/ */
PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE), PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE),
/** /**
* public * public
*/ */
PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE), PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE),
/** /**
* custom * custom
*/ */
CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ); CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ);
/** /**
* 权限类型数据库值 * 权限类型数据库值
@ -44,11 +44,6 @@ public enum AccessPolicyType {
*/ */
private final ObjectCannedACL objectCannedACL; private final ObjectCannedACL objectCannedACL;
/**
* 桶策略类型
*/
private final PolicyType policyType;
public static AccessPolicyType getByType(String type) { public static AccessPolicyType getByType(String type) {
for (AccessPolicyType value : values()) { for (AccessPolicyType value : values()) {
if (value.getType().equals(type)) { if (value.getType().equals(type)) {

View File

@ -1,35 +0,0 @@
package org.dromara.common.oss.enumd;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* minio策略配置
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum PolicyType {
/**
* 只读
*/
READ("read-only"),
/**
* 只写
*/
WRITE("write-only"),
/**
* 读写
*/
READ_WRITE("read-write");
/**
* 类型
*/
private final String type;
}

View File

@ -38,4 +38,10 @@ public @interface RateLimiter {
* 提示消息 支持国际化 格式为 {code} * 提示消息 支持国际化 格式为 {code}
*/ */
String message() default "{rate.limiter.message}"; String message() default "{rate.limiter.message}";
/**
* 限流策略超时时间 默认一天(策略存活时间 会清除已存在的策略数据)
*/
int timeout() default 86400;
} }

View File

@ -54,13 +54,14 @@ public class RateLimiterAspect {
public void doBefore(JoinPoint point, RateLimiter rateLimiter) { public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
int time = rateLimiter.time(); int time = rateLimiter.time();
int count = rateLimiter.count(); int count = rateLimiter.count();
int timeout = rateLimiter.timeout();
try { try {
String combineKey = getCombineKey(rateLimiter, point); String combineKey = getCombineKey(rateLimiter, point);
RateType rateType = RateType.OVERALL; RateType rateType = RateType.OVERALL;
if (rateLimiter.limitType() == LimitType.CLUSTER) { if (rateLimiter.limitType() == LimitType.CLUSTER) {
rateType = RateType.PER_CLIENT; rateType = RateType.PER_CLIENT;
} }
long number = RedisUtils.rateLimiter(combineKey, rateType, count, time); long number = RedisUtils.rateLimiter(combineKey, rateType, count, time, timeout);
if (number == -1) { if (number == -1) {
String message = rateLimiter.message(); String message = rateLimiter.message();
if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) { if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {

View File

@ -42,6 +42,18 @@
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
</dependency> </dependency>
<!-- &lt;!&ndash; redis序列化替代方案 比json快无数的跨语言二进制序列化 &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.apache.fury</groupId>-->
<!-- <artifactId>fury-core</artifactId>-->
<!-- <version>0.9.0</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-api</artifactId>-->
<!-- </dependency>-->
</dependencies> </dependencies>
</project> </project>

View File

@ -53,6 +53,9 @@ public class RedisConfig {
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型类必须是非final修饰的序列化时将对象全类名一起保存下来 // 指定序列化输入的类型类必须是非final修饰的序列化时将对象全类名一起保存下来
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// LoggerFactory.useSlf4jLogging(true);
// FuryCodec furyCodec = new FuryCodec();
// CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, furyCodec, furyCodec);
TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om); TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
// 组合序列化 key 使用 String 内容使用通用 json 格式 // 组合序列化 key 使用 String 内容使用通用 json 格式
CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec); CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);

View File

@ -174,12 +174,12 @@ public class QueueUtils {
* *
* @param queueName 队列名 * @param queueName 队列名
* @param capacity 容量 * @param capacity 容量
* @param destroy 已存在是否销毁 * @param destroy 是否销毁
*/ */
public static <T> boolean trySetBoundedQueueCapacity(String queueName, int capacity, boolean destroy) { public static <T> boolean trySetBoundedQueueCapacity(String queueName, int capacity, boolean destroy) {
RBoundedBlockingQueue<T> boundedBlockingQueue = CLIENT.getBoundedBlockingQueue(queueName); RBoundedBlockingQueue<T> boundedBlockingQueue = CLIENT.getBoundedBlockingQueue(queueName);
if (boundedBlockingQueue.isExists() && destroy) { if (destroy) {
destroyQueue(queueName); boundedBlockingQueue.delete();
} }
return boundedBlockingQueue.trySetCapacity(capacity); return boundedBlockingQueue.trySetCapacity(capacity);
} }

View File

@ -4,6 +4,7 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.redisson.api.*; import org.redisson.api.*;
import org.redisson.api.options.KeysScanOptions;
import java.time.Duration; import java.time.Duration;
import java.util.Collection; import java.util.Collection;
@ -36,8 +37,22 @@ public class RedisUtils {
* @return -1 表示失败 * @return -1 表示失败
*/ */
public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) { public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) {
return rateLimiter(key, rateType, rate, rateInterval, 0);
}
/**
* 限流
*
* @param key 限流key
* @param rateType 限流类型
* @param rate 速率
* @param rateInterval 速率间隔
* @param timeout 超时时间
* @return -1 表示失败
*/
public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval, int timeout) {
RRateLimiter rateLimiter = CLIENT.getRateLimiter(key); RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS); rateLimiter.trySetRate(rateType, rate, Duration.ofSeconds(rateInterval), Duration.ofSeconds(timeout));
if (rateLimiter.tryAcquire()) { if (rateLimiter.tryAcquire()) {
return rateLimiter.availablePermits(); return rateLimiter.availablePermits();
} else { } else {
@ -518,13 +533,34 @@ public class RedisUtils {
/** /**
* 获得缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id) * 获得缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id)
* * <P>
* limit-设置扫描的限制数量(默认为0,查询全部)
* pattern-设置键的匹配模式(默认为null)
* chunkSize-设置每次扫描的块大小(默认为0,本方法设置为1000)
* type-设置键的类型(默认为null,查询全部类型)
* </P>
* @see KeysScanOptions
* @param pattern 字符串前缀 * @param pattern 字符串前缀
* @return 对象列表 * @return 对象列表
*/ */
public static Collection<String> keys(final String pattern) { public static Collection<String> keys(final String pattern) {
Stream<String> stream = CLIENT.getKeys().getKeysStreamByPattern(pattern); return keys(KeysScanOptions.defaults().pattern(pattern).chunkSize(1000));
return stream.collect(Collectors.toList()); }
/**
* 通过扫描参数获取缓存的基本对象列表
* @param keysScanOptions 扫描参数
* <P>
* limit-设置扫描的限制数量(默认为0,查询全部)
* pattern-设置键的匹配模式(默认为null)
* chunkSize-设置每次扫描的块大小(默认为0)
* type-设置键的类型(默认为null,查询全部类型)
* </P>
* @see KeysScanOptions
*/
public static Collection<String> keys(final KeysScanOptions keysScanOptions) {
Stream<String> keysStream = CLIENT.getKeys().getKeysStream(keysScanOptions);
return keysStream.collect(Collectors.toList());
} }
/** /**

View File

@ -0,0 +1,165 @@
package org.dromara.common.redis.utils;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.redisson.api.RIdGenerator;
import org.redisson.api.RedissonClient;
import java.time.Duration;
/**
* 发号器工具类
*
* @author 秋辞未寒
* @date 2024-12-10
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SequenceUtils {
/**
* 默认初始值
*/
public static final Long DEFAULT_INIT_VALUE = 1L;
/**
* 默认步长
*/
public static final Long DEFAULT_STEP_VALUE = 1L;
/**
* 默认过期时间-
*/
public static final Duration DEFAULT_EXPIRE_TIME_DAY = Duration.ofDays(1);
/**
* 默认过期时间-分钟
*/
public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1);
/**
* 获取Redisson客户端实例
*/
private static final RedissonClient REDISSON_CLIENT = SpringUtils.getBean(RedissonClient.class);
/**
* 获取ID生成器
*
* @param key 业务key
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return ID生成器
*/
private static RIdGenerator getIdGenerator(String key, Duration expireTime, Long initValue, Long stepValue) {
if (initValue == null || initValue <= 0) {
initValue = DEFAULT_INIT_VALUE;
}
if (stepValue == null || stepValue <= 0) {
stepValue = DEFAULT_STEP_VALUE;
}
RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key);
// 设置过期时间
idGenerator.expire(expireTime);
// 设置初始值和步长
idGenerator.tryInit(initValue, stepValue);
return idGenerator;
}
/**
* 获取指定业务key的唯一id
*
* @param key 业务key
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static long nextId(String key, Duration expireTime, Long initValue, Long stepValue) {
return getIdGenerator(key, expireTime, initValue, stepValue).nextId();
}
/**
* 获取指定业务key的唯一id字符串
*
* @param key 业务key
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static String nextIdStr(String key, Duration expireTime, Long initValue, Long stepValue) {
return String.valueOf(nextId(key, expireTime, initValue, stepValue));
}
/**
* 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
*
* @param key 业务key
* @param expireTime 过期时间
* @return 唯一id
*/
public static long nextId(String key, Duration expireTime) {
return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
}
/**
* 获取指定业务key的唯一id字符串 (ID初始值=1,ID步长=1)
*
* @param key 业务key
* @param expireTime 过期时间
* @return 唯一id
*/
public static String nextIdStr(String key, Duration expireTime) {
return String.valueOf(nextId(key, expireTime));
}
/**
* 获取 yyyyMMdd 开头的唯一id
*
* @return 唯一id
*/
public static String nextIdDate() {
return nextIdDate("");
}
/**
* 获取 prefix + yyyyMMdd 开头的唯一id
*
* @param prefix 业务前缀
* @return 唯一id
*/
public static String nextIdDate(String prefix) {
// 前缀+日期 构建 prefixKey
String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMATTER));
// 获取下一个id
long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_DAY, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
// 返回完整id
return StringUtils.format("{}{}", prefixKey, nextId);
}
/**
* 获取 yyyyMMddHHmmss 开头的唯一id
*
* @return 唯一id
*/
public static String nextIdDateTime() {
return nextIdDateTime("");
}
/**
* 获取 prefix + yyyyMMddHHmmss 开头的唯一id
*
* @param prefix 业务前缀
* @return 唯一id
*/
public static String nextIdDateTime(String prefix) {
// 前缀+日期时间 构建 prefixKey
String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMATTER));
// 获取下一个id
long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_MINUTE, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
// 返回完整id
return StringUtils.format("{}{}", prefixKey, nextId);
}
}

View File

@ -8,8 +8,8 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.constant.TenantConstants; import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.constant.UserConstants;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.UserType; import org.dromara.common.core.enums.UserType;
@ -88,6 +88,13 @@ public class LoginHelper {
return Convert.toLong(getExtra(USER_KEY)); return Convert.toLong(getExtra(USER_KEY));
} }
/**
* 获取用户id
*/
public static String getUserIdStr() {
return Convert.toStr(getExtra(USER_KEY));
}
/** /**
* 获取用户账户 * 获取用户账户
*/ */
@ -152,7 +159,7 @@ public class LoginHelper {
* @return 结果 * @return 结果
*/ */
public static boolean isSuperAdmin(Long userId) { public static boolean isSuperAdmin(Long userId) {
return UserConstants.SUPER_ADMIN_ID.equals(userId); return SystemConstants.SUPER_ADMIN_ID.equals(userId);
} }
/** /**

View File

@ -22,7 +22,13 @@ import java.lang.annotation.Target;
public @interface Sensitive { public @interface Sensitive {
SensitiveStrategy strategy(); SensitiveStrategy strategy();
String roleKey() default ""; /**
* 角色标识符 多个角色满足一个即可
*/
String[] roleKey() default {};
String perms() default ""; /**
* 权限标识符 多个权限满足一个即可
*/
String[] perms() default {};
} }

View File

@ -13,6 +13,6 @@ public interface SensitiveService {
/** /**
* 是否脱敏 * 是否脱敏
*/ */
boolean isSensitive(String roleKey, String perms); boolean isSensitive(String[] roleKey, String[] perms);
} }

View File

@ -80,12 +80,12 @@ public enum SensitiveStrategy {
FIRST_MASK(DesensitizedUtil::firstMask), FIRST_MASK(DesensitizedUtil::firstMask),
/** /**
* 清空为null * 清空为""
*/ */
CLEAR(s -> DesensitizedUtil.clear()), CLEAR(s -> DesensitizedUtil.clear()),
/** /**
* 清空为"" * 清空为null
*/ */
CLEAR_TO_NULL(s -> DesensitizedUtil.clearToNull()); CLEAR_TO_NULL(s -> DesensitizedUtil.clearToNull());

View File

@ -26,8 +26,8 @@ import java.util.Objects;
public class SensitiveHandler extends JsonSerializer<String> implements ContextualSerializer { public class SensitiveHandler extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveStrategy strategy; private SensitiveStrategy strategy;
private String roleKey; private String[] roleKey;
private String perms; private String[] perms;
@Override @Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {

View File

@ -66,7 +66,7 @@ public class PlusSmsDao implements SmsDao {
*/ */
@Override @Override
public void clean() { public void clean() {
RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + "sms:"); RedisUtils.deleteKeys(GlobalConstants.GLOBAL_REDIS_KEY + "sms:*");
} }
} }

View File

@ -30,7 +30,7 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
} }
@Override @Override
protected AuthToken getAccessToken(AuthCallback authCallback) { public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode()); String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body); Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常 // oauth/token 验证异常
@ -51,7 +51,7 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
} }
@Override @Override
protected AuthUser getUserInfo(AuthToken authToken) { public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken); String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body); Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常 // oauth/token 验证异常

View File

@ -1,7 +1,10 @@
package org.dromara.common.social.topiam; package org.dromara.common.social.topiam;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.xkcoding.http.support.HttpHeader; import com.xkcoding.http.support.HttpHeader;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.cache.AuthStateCache; import me.zhyd.oauth.cache.AuthStateCache;
@ -41,7 +44,7 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
} }
@Override @Override
protected AuthToken getAccessToken(AuthCallback authCallback) { public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode()); String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body); Dict object = JsonUtils.parseMap(body);
checkResponse(object); checkResponse(object);
@ -55,7 +58,7 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
} }
@Override @Override
protected AuthUser getUserInfo(AuthToken authToken) { public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken); String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body); Dict object = JsonUtils.parseMap(body);
checkResponse(object); checkResponse(object);
@ -70,6 +73,16 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
.build(); .build();
} }
@Override
protected String doPostAuthorizationCode(String code) {
HttpRequest request = HttpRequest.post(source.accessToken())
.header("Authorization", "Basic " + Base64.encode("%s:%s".formatted(config.getClientId(), config.getClientSecret())))
.form("grant_type", "authorization_code")
.form("code", code)
.form("redirect_uri", config.getRedirectUri());
HttpResponse response = request.execute();
return response.body();
}
@Override @Override
protected String doGetUserInfo(AuthToken authToken) { protected String doGetUserInfo(AuthToken authToken) {
@ -86,7 +99,7 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
.build(); .build();
} }
public static void checkResponse(Dict object) { private static void checkResponse(Dict object) {
// oauth/token 验证异常 // oauth/token 验证异常
if (object.containsKey("error")) { if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description")); throw new AuthException(object.getStr("error_description"));

View File

@ -58,9 +58,9 @@ public class SocialUtils {
case "linkedin" -> new AuthLinkedinRequest(builder.build(), STATE_CACHE); case "linkedin" -> new AuthLinkedinRequest(builder.build(), STATE_CACHE);
case "microsoft" -> new AuthMicrosoftRequest(builder.build(), STATE_CACHE); case "microsoft" -> new AuthMicrosoftRequest(builder.build(), STATE_CACHE);
case "renren" -> new AuthRenrenRequest(builder.build(), STATE_CACHE); case "renren" -> new AuthRenrenRequest(builder.build(), STATE_CACHE);
case "stack_overflow" -> new AuthStackOverflowRequest(builder.build(), STATE_CACHE); case "stack_overflow" -> new AuthStackOverflowRequest(builder.stackOverflowKey(obj.getStackOverflowKey()).build(), STATE_CACHE);
case "huawei" -> new AuthHuaweiRequest(builder.build(), STATE_CACHE); case "huawei" -> new AuthHuaweiRequest(builder.build(), STATE_CACHE);
case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(builder.build(), STATE_CACHE); case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(builder.agentId(obj.getAgentId()).build(), STATE_CACHE);
case "gitlab" -> new AuthGitlabRequest(builder.build(), STATE_CACHE); case "gitlab" -> new AuthGitlabRequest(builder.build(), STATE_CACHE);
case "wechat_mp" -> new AuthWeChatMpRequest(builder.build(), STATE_CACHE); case "wechat_mp" -> new AuthWeChatMpRequest(builder.build(), STATE_CACHE);
case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE); case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE);

View File

@ -67,7 +67,9 @@ public class SseEmitterManager {
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId); Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
if (emitters != null) { if (emitters != null) {
try { try {
emitters.get(token).send(SseEmitter.event().comment("disconnected")); SseEmitter sseEmitter = emitters.get(token);
sseEmitter.send(SseEmitter.event().comment("disconnected"));
sseEmitter.complete();
} catch (Exception ignore) { } catch (Exception ignore) {
} }
emitters.remove(token); emitters.remove(token);

View File

@ -16,6 +16,7 @@ import org.dromara.common.sse.dto.SseMessageDto;
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SseMessageUtils { public class SseMessageUtils {
private final static Boolean SSE_ENABLE = SpringUtils.getProperty("sse.enabled", Boolean.class, true);
private final static SseEmitterManager MANAGER = SpringUtils.getBean(SseEmitterManager.class); private final static SseEmitterManager MANAGER = SpringUtils.getBean(SseEmitterManager.class);
/** /**
@ -25,6 +26,9 @@ public class SseMessageUtils {
* @param message 要发送的消息内容 * @param message 要发送的消息内容
*/ */
public static void sendMessage(Long userId, String message) { public static void sendMessage(Long userId, String message) {
if (!isEnable()) {
return;
}
MANAGER.sendMessage(userId, message); MANAGER.sendMessage(userId, message);
} }
@ -34,6 +38,9 @@ public class SseMessageUtils {
* @param message 要发送的消息内容 * @param message 要发送的消息内容
*/ */
public static void sendMessage(String message) { public static void sendMessage(String message) {
if (!isEnable()) {
return;
}
MANAGER.sendMessage(message); MANAGER.sendMessage(message);
} }
@ -43,6 +50,9 @@ public class SseMessageUtils {
* @param sseMessageDto 要发布的SSE消息对象 * @param sseMessageDto 要发布的SSE消息对象
*/ */
public static void publishMessage(SseMessageDto sseMessageDto) { public static void publishMessage(SseMessageDto sseMessageDto) {
if (!isEnable()) {
return;
}
MANAGER.publishMessage(sseMessageDto); MANAGER.publishMessage(sseMessageDto);
} }
@ -52,7 +62,17 @@ public class SseMessageUtils {
* @param message 要发布的消息内容 * @param message 要发布的消息内容
*/ */
public static void publishAll(String message) { public static void publishAll(String message) {
if (!isEnable()) {
return;
}
MANAGER.publishAll(message); MANAGER.publishAll(message);
} }
/**
* 是否开启
*/
public static Boolean isEnable() {
return SSE_ENABLE;
}
} }

View File

@ -4,7 +4,6 @@ import cn.dev33.satoken.dao.SaTokenDao;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.dromara.common.core.utils.reflect.ReflectUtils; import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.mybatis.config.MybatisPlusConfig;
import org.dromara.common.redis.config.RedisConfig; import org.dromara.common.redis.config.RedisConfig;
import org.dromara.common.redis.config.properties.RedissonProperties; import org.dromara.common.redis.config.properties.RedissonProperties;
import org.dromara.common.tenant.core.TenantSaTokenDao; import org.dromara.common.tenant.core.TenantSaTokenDao;
@ -16,7 +15,7 @@ import org.redisson.config.ClusterServersConfig;
import org.redisson.config.SingleServerConfig; import org.redisson.config.SingleServerConfig;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer; import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
@ -33,8 +32,8 @@ import org.springframework.context.annotation.Primary;
@ConditionalOnProperty(value = "tenant.enable", havingValue = "true") @ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
public class TenantConfig { public class TenantConfig {
@ConditionalOnBean(MybatisPlusConfig.class) @ConditionalOnClass(TenantLineInnerInterceptor.class)
@AutoConfiguration(after = {MybatisPlusConfig.class}) @AutoConfiguration
static class MybatisPlusConfiguration { static class MybatisPlusConfiguration {
/** /**

View File

@ -27,8 +27,12 @@ public class TenantKeyPrefixHandler extends KeyPrefixHandler {
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
return null; return null;
} }
if (InterceptorIgnoreHelper.willIgnoreTenantLine("")) { try {
return super.map(name); if (InterceptorIgnoreHelper.willIgnoreTenantLine("")) {
return super.map(name);
}
} catch (NoClassDefFoundError ignore) {
// 有些服务不需要mp导致类不存在 忽略即可
} }
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) { if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.map(name); return super.map(name);
@ -54,8 +58,12 @@ public class TenantKeyPrefixHandler extends KeyPrefixHandler {
if (StringUtils.isBlank(unmap)) { if (StringUtils.isBlank(unmap)) {
return null; return null;
} }
if (InterceptorIgnoreHelper.willIgnoreTenantLine("")) { try {
return super.unmap(name); if (InterceptorIgnoreHelper.willIgnoreTenantLine("")) {
return super.unmap(name);
}
} catch (NoClassDefFoundError ignore) {
// 有些服务不需要mp导致类不存在 忽略即可
} }
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) { if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.unmap(name); return super.unmap(name);

View File

@ -39,7 +39,7 @@ public class TranslationHandler extends JsonSerializer<Object> implements Contex
if (ObjectUtil.isNotNull(trans)) { if (ObjectUtil.isNotNull(trans)) {
// 如果映射字段不为空 则取映射字段的值 // 如果映射字段不为空 则取映射字段的值
if (StringUtils.isNotBlank(translation.mapper())) { if (StringUtils.isNotBlank(translation.mapper())) {
value = ReflectUtils.invokeGetter(gen.getCurrentValue(), translation.mapper()); value = ReflectUtils.invokeGetter(gen.currentValue(), translation.mapper());
} }
// 如果为 null 直接写出 // 如果为 null 直接写出
if (ObjectUtil.isNull(value)) { if (ObjectUtil.isNull(value)) {

View File

@ -1,6 +1,8 @@
package org.dromara.common.web.config; package org.dromara.common.web.config;
import io.undertow.server.DefaultByteBufferPool; import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.handlers.DisallowedMethodsHandler;
import io.undertow.util.HttpString;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo; import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
@ -16,18 +18,45 @@ import org.springframework.core.task.VirtualThreadTaskExecutor;
@AutoConfiguration @AutoConfiguration
public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> { public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
/**
* 自定义 Undertow 配置
* <p>
* 主要配置内容包括
* 1. 配置 WebSocket 部署信息
* 2. 在虚拟线程模式下使用虚拟线程池
* 3. 禁用不安全的 HTTP 方法 CONNECTTRACETRACK
* </p>
*
* @param factory Undertow Web 服务器工厂
*/
@Override @Override
public void customize(UndertowServletWebServerFactory factory) { public void customize(UndertowServletWebServerFactory factory) {
factory.addDeploymentInfoCustomizers(deploymentInfo -> { factory.addDeploymentInfoCustomizers(deploymentInfo -> {
// 配置 WebSocket 部署信息设置 WebSocket 使用的缓冲区池
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo(); WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 1024)); webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 1024));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo); deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
// 使用虚拟线程
// 如果启用了虚拟线程配置 Undertow 使用虚拟线程池
if (SpringUtils.isVirtual()) { if (SpringUtils.isVirtual()) {
// 创建虚拟线程池线程池前缀为 "undertow-"
VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("undertow-"); VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("undertow-");
// 设置虚拟线程池为执行器和异步执行器
deploymentInfo.setExecutor(executor); deploymentInfo.setExecutor(executor);
deploymentInfo.setAsyncExecutor(executor); deploymentInfo.setAsyncExecutor(executor);
} }
// 配置禁止某些不安全的 HTTP 方法 CONNECTTRACETRACK
deploymentInfo.addInitialHandlerChainWrapper(handler -> {
// 禁止三个方法 CONNECT/TRACE/TRACK 也是不安全的 避免爬虫骚扰
HttpString[] disallowedHttpMethods = {
HttpString.tryFromString("CONNECT"),
HttpString.tryFromString("TRACE"),
HttpString.tryFromString("TRACK")
};
// 使用 DisallowedMethodsHandler 拦截并拒绝这些方法的请求
return new DisallowedMethodsHandler(handler, disallowedHttpMethods);
});
}); });
} }

View File

@ -63,7 +63,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(SseException.class) @ExceptionHandler(SseException.class)
public String handleNotLoginException(SseException e, HttpServletRequest request) { public String handleNotLoginException(SseException e, HttpServletRequest request) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage()); log.debug("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return JsonUtils.toJsonString(R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源")); return JsonUtils.toJsonString(R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源"));
} }

View File

@ -35,6 +35,12 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId> <artifactId>spring-boot-starter-websocket</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-debian:17.0.11-cds FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
#FROM bellsoft/liberica-openjdk-debian:21.0.3-cds #FROM bellsoft/liberica-openjdk-debian:21.0.5-cds
#FROM findepi/graalvm:java17-native #FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li" LABEL maintainer="Lion Li"
@ -15,6 +15,8 @@ EXPOSE 9090
ADD ./target/ruoyi-monitor-admin.jar ./app.jar ADD ./target/ruoyi-monitor-admin.jar ./app.jar
SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \ ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \ -XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
-jar app.jar -jar app.jar

View File

@ -19,7 +19,6 @@ spring:
admin: admin:
ui: ui:
title: RuoYi-Vue-Plus服务监控中心 title: RuoYi-Vue-Plus服务监控中心
context-path: /admin
--- # Actuator 监控端点的配置项 --- # Actuator 监控端点的配置项
management: management:
@ -38,7 +37,7 @@ spring.boot.admin.client:
# 增加客户端开关 # 增加客户端开关
enabled: true enabled: true
# 设置 Spring Boot Admin Server 地址 # 设置 Spring Boot Admin Server 地址
url: http://localhost:9090/admin url: http://localhost:9090
instance: instance:
service-host-type: IP service-host-type: IP
metadata: metadata:

View File

@ -4,7 +4,7 @@
<contextName>logback</contextName> <contextName>logback</contextName>
<property name="log.path" value="./logs/ruoyi-monitor-admin"/> <property name="log.path" value="./logs/ruoyi-monitor-admin"/>
<property name="console.log.pattern" <property name="console.log.pattern"
value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/> value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/> <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
@ -31,4 +31,4 @@
<appender-ref ref="file"/> <appender-ref ref="file"/>
</root> </root>
</configuration> </configuration>

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-debian:17.0.11-cds FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
#FROM bellsoft/liberica-openjdk-debian:21.0.3-cds #FROM bellsoft/liberica-openjdk-debian:21.0.5-cds
#FROM findepi/graalvm:java17-native #FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li" LABEL maintainer="Lion Li"
@ -16,6 +16,8 @@ EXPOSE 17888
ADD ./target/ruoyi-snailjob-server.jar ./app.jar ADD ./target/ruoyi-snailjob-server.jar ./app.jar
SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \ ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \ -XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
-jar app.jar -jar app.jar

View File

@ -22,7 +22,7 @@ public class SecurityConfig {
public FilterRegistrationBean<ActuatorAuthFilter> actuatorFilterRegistrationBean() { public FilterRegistrationBean<ActuatorAuthFilter> actuatorFilterRegistrationBean() {
FilterRegistrationBean<ActuatorAuthFilter> registrationBean = new FilterRegistrationBean<>(); FilterRegistrationBean<ActuatorAuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ActuatorAuthFilter(username, password)); registrationBean.setFilter(new ActuatorAuthFilter(username, password));
registrationBean.addUrlPatterns("/actuator", "/actuator/**"); registrationBean.addUrlPatterns("/actuator", "/actuator/*");
return registrationBean; return registrationBean;
} }

View File

@ -40,7 +40,7 @@ snail-job:
spring.boot.admin.client: spring.boot.admin.client:
# 增加客户端开关 # 增加客户端开关
enabled: true enabled: true
url: http://localhost:9090/admin url: http://localhost:9090
instance: instance:
service-host-type: IP service-host-type: IP
metadata: metadata:

View File

@ -40,7 +40,7 @@ snail-job:
spring.boot.admin.client: spring.boot.admin.client:
# 增加客户端开关 # 增加客户端开关
enabled: true enabled: true
url: http://localhost:9090/admin url: http://localhost:9090
instance: instance:
service-host-type: IP service-host-type: IP
metadata: metadata:

View File

@ -2,7 +2,7 @@
<configuration> <configuration>
<property name="log.path" value="./logs/ruoyi-snailjob-server" /> <property name="log.path" value="./logs/ruoyi-snailjob-server" />
<property name="console.log.pattern" <property name="console.log.pattern"
value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/> value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/> <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>

View File

@ -1,14 +1,16 @@
package org.dromara.demo.controller; package org.dromara.demo.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.mail.utils.MailUtils; import org.dromara.common.mail.utils.MailUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.io.File; import java.io.File;
import java.util.Arrays;
/** /**
@ -16,6 +18,7 @@ import java.io.File;
* *
* @author Michelle.Chung * @author Michelle.Chung
*/ */
@SaIgnore
@Validated @Validated
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@ -49,4 +52,19 @@ public class MailController {
return R.ok(); return R.ok();
} }
/**
* 发送邮件多附件
*
* @param to 接收人
* @param subject 标题
* @param text 内容
* @param paths 附件路径
*/
@GetMapping("/sendMessageWithAttachments")
public R<Void> sendMessageWithAttachments(String to, String subject, String text, String[] paths) {
File[] array = Arrays.stream(paths).map(File::new).toArray(File[]::new);
MailUtils.sendText(to, subject, text, array);
return R.ok();
}
} }

View File

@ -1,5 +1,6 @@
package org.dromara.demo.controller; package org.dromara.demo.controller;
import cn.hutool.core.thread.ThreadUtil;
import org.dromara.common.core.constant.CacheNames; import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
@ -83,11 +84,7 @@ public class RedisCacheController {
RedisUtils.setCacheObject(key, value); RedisUtils.setCacheObject(key, value);
boolean flag = RedisUtils.expire(key, Duration.ofSeconds(10)); boolean flag = RedisUtils.expire(key, Duration.ofSeconds(10));
System.out.println("***********" + flag); System.out.println("***********" + flag);
try { ThreadUtil.sleep(11 * 1000);
Thread.sleep(11 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object obj = RedisUtils.getCacheObject(key); Object obj = RedisUtils.getCacheObject(key);
return R.ok(value.equals(obj)); return R.ok(value.equals(obj));
} }

View File

@ -1,5 +1,6 @@
package org.dromara.demo.controller; package org.dromara.demo.controller;
import cn.hutool.core.thread.ThreadUtil;
import com.baomidou.lock.LockInfo; import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate; import com.baomidou.lock.LockTemplate;
import com.baomidou.lock.annotation.Lock4j; import com.baomidou.lock.annotation.Lock4j;
@ -33,13 +34,9 @@ public class RedisLockController {
@Lock4j(keys = {"#key"}) @Lock4j(keys = {"#key"})
@GetMapping("/testLock4j") @GetMapping("/testLock4j")
public R<String> testLock4j(String key, String value) { public R<String> testLock4j(String key, String value) {
System.out.println("start:" + key + ",time:" + LocalTime.now().toString()); System.out.println("start:" + key + ",time:" + LocalTime.now());
try { ThreadUtil.sleep(10000);
Thread.sleep(10000); System.out.println("end :" + key + ",time:" + LocalTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end :" + key + ",time:" + LocalTime.now().toString());
return R.ok("操作成功", value); return R.ok("操作成功", value);
} }
@ -54,11 +51,7 @@ public class RedisLockController {
} }
// 获取锁成功处理业务 // 获取锁成功处理业务
try { try {
try { ThreadUtil.sleep(8000);
Thread.sleep(8000);
} catch (InterruptedException e) {
//
}
System.out.println("执行简单方法1 , 当前线程:" + Thread.currentThread().getName()); System.out.println("执行简单方法1 , 当前线程:" + Thread.currentThread().getName());
} finally { } finally {
//释放锁 //释放锁

View File

@ -15,6 +15,9 @@ import org.dromara.common.excel.annotation.ExcelEnumFormat;
import org.dromara.common.excel.convert.ExcelDictConvert; import org.dromara.common.excel.convert.ExcelDictConvert;
import org.dromara.common.excel.convert.ExcelEnumConvert; import org.dromara.common.excel.convert.ExcelEnumConvert;
import java.io.Serial;
import java.io.Serializable;
/** /**
* 带有下拉选的Excel导出 * 带有下拉选的Excel导出
* *
@ -24,8 +27,9 @@ import org.dromara.common.excel.convert.ExcelEnumConvert;
@ExcelIgnoreUnannotated @ExcelIgnoreUnannotated
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class ExportDemoVo { public class ExportDemoVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**

View File

@ -2,6 +2,8 @@ package org.dromara.demo.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelNotation;
import org.dromara.common.excel.annotation.ExcelRequired;
import org.dromara.common.translation.annotation.Translation; import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant; import org.dromara.common.translation.constant.TransConstant;
import org.dromara.demo.domain.TestDemo; import org.dromara.demo.domain.TestDemo;
@ -36,30 +38,35 @@ public class TestDemoVo implements Serializable {
/** /**
* 部门id * 部门id
*/ */
@ExcelRequired
@ExcelProperty(value = "部门id") @ExcelProperty(value = "部门id")
private Long deptId; private Long deptId;
/** /**
* 用户id * 用户id
*/ */
@ExcelRequired
@ExcelProperty(value = "用户id") @ExcelProperty(value = "用户id")
private Long userId; private Long userId;
/** /**
* 排序号 * 排序号
*/ */
@ExcelRequired
@ExcelProperty(value = "排序号") @ExcelProperty(value = "排序号")
private Integer orderNum; private Integer orderNum;
/** /**
* key键 * key键
*/ */
@ExcelNotation(value = "测试key")
@ExcelProperty(value = "key键") @ExcelProperty(value = "key键")
private String testKey; private String testKey;
/** /**
* *
*/ */
@ExcelNotation(value = "测试value")
@ExcelProperty(value = "") @ExcelProperty(value = "")
private String value; private String value;

View File

@ -34,14 +34,18 @@ public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
@DataColumn(key = "deptName", value = "dept_id"), @DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id") @DataColumn(key = "userName", value = "user_id")
}) })
List<TestDemo> selectList(IPage<TestDemo> page, @Param(Constants.WRAPPER) Wrapper<TestDemo> queryWrapper); default <P extends IPage<TestDemoVo>> P selectVoPage(IPage<TestDemo> page, Wrapper<TestDemo> wrapper) {
return selectVoPage(page, wrapper, this.currentVoClass());
}
@Override @Override
@DataPermission({ @DataPermission({
@DataColumn(key = "deptName", value = "dept_id"), @DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id") @DataColumn(key = "userName", value = "user_id")
}) })
List<TestDemo> selectList(@Param(Constants.WRAPPER) Wrapper<TestDemo> queryWrapper); default List<TestDemoVo> selectVoList(Wrapper<TestDemo> wrapper) {
return selectVoList(wrapper, this.currentVoClass());
}
@Override @Override
@DataPermission(value = { @DataPermission(value = {

Some files were not shown because too many files have changed in this diff Show More